0
点赞
收藏
分享

微信扫一扫

Java乐观锁与CAS

烟中雯城 2022-03-24 阅读 66
java

本文内容及部分代码图片参考视频:https://www.bilibili.com/video/BV1ff4y1q7we?spm_id_from=333.999.0.0

1.乐观锁与悲观锁的区别

Java中处理多线程调用同一个资源对象时有两种处理方式,第一种:操作系统悲观的认为接下来的操作者中,如果不能严格的同步线程调用,那么一定会出现数据不一致等异常,因此需要需要在一开始就使用锁将资源对象锁定,一个时刻只供一个线程调用,其它线程将会被阻塞,因此这种同步机制也被称为悲观锁;第二种:系统认为接下来多线程的操作不一定会引起数据不一致等问题,因此不会一开始就对资源对象加锁,而是再有更新数据时对比原数据是否有发送变化,从而判断是否会产生数据不一致等问题,这种同步机制被称为乐观锁。下面,我们对这两种锁机制进行简介如下:

1.1悲观锁

悲观锁对数据被外界修改保留悲观态度,认为一定会出现线程不安全问题。因此,它对资源对象具有强烈的独占和排他性,在整个资源对象调用期间,会一直保持锁定状态,Java中常用互斥锁实现,在关系型数据库中常见为读写锁,表锁,行锁;

1.2乐观锁

乐观锁在每次调用资源对象时都认为其它线程不会对资源对象进行修改,因此也不会对其进行上锁,但在它更新资源对象时会判断一下资源对象有没有发生改变,如果有则重新读取资源对象重新执行操作。乐观锁多用于读操作频繁的场景,相较于悲观锁,它可以很大程度上减少系统在用户态和内核态之间进行切换,从而提高系统整体的效率。

2.CAS

CAS,全程Compare And Swap,即对比与交换,是乐观锁的底层实现基础。我们举一个简单的例子来说明CAS操作,假设现在有一个房子,它有一个门牌号为1,现在线程A到达房子门口,它获取到门牌号1,然后去执行其它操作,这时线程B过来,也获取到门牌号1,然后将门牌号改为0,此时A线程回来想要修改门牌号,它先要对比此时的门牌号与它之前获取的是否一样,然后发现门牌号成为了0,那么它就不能在执行更改操作,需要重新获取门牌号,然后再去执行业务逻辑,返回后在对比门牌号是否改变,没有就可以执行更改操作。,下面用一段代码说明;

int cas(long *addr, long oldValue, long newValue)
{

 if(*addr != old)
     return 0;
 *addr=new;
 return 1;
}

其实就是简单的进行对比新旧两个值是否发生变化。但看上面的代码不难发现其存在两个问题。

①如果我们对比的值设置为普通的int或者long一类的就可能会丢失更改,比如如果线程A得到oldValue为1,线程B也得到oldValue为1,然后B将oldValue修改为2,然后重新记录oldValue为2,过了一会B又将oldValue更改为1,那么如果此时A线程过来,它就会误以为oldValue的值没有发生过改变。对于这个问题,我们的解决办法是使用专门的版本号或者时间戳做为资源对象是否发生改变的标志;

②如果A线程通过对比oldValue发现没有发生改变,开始进行对资源对象进行修改;但如果在A还没有完成修改,oldValue值还没有发生变化时过来一个B线程,它对比发现oldValue值没有改变也开始进行修改操作,这样就会导致A,B线程中有一个的更新被覆盖,产生线程不安全。对于这个问题,我们可以看出,要解决它,就一定要保证比较与更新这两个操作是原子的,而如何保证这两个操作是原子的,这其实就不用我们编程去实现,因为不同架构的CPU指令集都已经为我们实现了这个操作,如X86指令集中使用cmpxchg指令实现原子级别的CAS操作,ARM下则使用LL/SS指令。

3.Java中使用CAS进行乐观锁编程

在通常我们进行编程过程中,经常会出现多线程竞争问题,以简单的多线程控制数字递增代码为例,如果我们使用简单的synchronized关键字实现线程同步,代码如下:

static Integer num = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 3; i++) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (num < 1000) {
                    synchronized (Main.class) {
                            num++;
                            System.out.println(Thread.currentThread().getName()
                        +":" + num);
                    }
                }
            }
        });
        t.start();
    }
}

在上面的代码里面里面我们使用synchronized实现线程同步,但这种锁是悲观的,会调用操作系统mutex lock原语使得系统从用户态切换至内核态,耗费性能,因此我们使用CAS的乐观锁机制去避免这个问题,具体代码如下:

static AtomicInteger num = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 3; i++) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (num。get() < 1000) {
                  System.out.println(Thread.currentThread().getName()
                        +":" + num.incrementAndGet());
                }
            }
        });
        t.start();
    }
}

这里看见其实我们只是简单的把num的类型转换成了AtomicInteger,而这个AtomicInteger就是JUC里面使用CAS为我们实现的一个同步计数器,下面我们看一下它的源码内容:

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

这是AtomicInteger中incrementAndGet()方法的实现,我们看见他内部调用了unsafe.getAndAddInt()方法,unsafe是Java中专门提供的一个用于操作计算机系统底层资源的类,因为这些操作要求权限高且容易引发系统不安全问题,因此命名为unsafe,我们继续进入unsafe.getAndAddInt()方法,代码如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }




public final native boolean compareAndSwapInt(Object var1, 
                                  long var2, int var4, int var5);

我们可以看出其内部调用了一个compareAndSwapInt()方法,这个方法使用native修饰,代表它是一个Java底层使用C++编写的代码且和你当前使用的操作系统有关,如果在进入其中就会发现它所使用了指令集命令cmpxchg实现CAS操作。

总结

乐观锁与悲观锁的主要区别就是就是在于锁的使用,悲观锁使用互斥实现,底层会使用mutex lock原语去实现同步,会引起系统从用户态和内核态之间切换,从而影响系统的性能(可以使用锁升级缓解),而乐观锁则是利用CAS实现无锁编程,它可以在提高系统性能,但不能代替锁。

举报

相关推荐

0 条评论