Java工程师面试1000题225-CAS原理解析

225、CAS原理解析

在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基百科)。

要讲CAS就不得不讲一下CAS的核心类-Unsafe类,由于Java方法无法直接访问底层系统,需要通过本地方法(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C语言的指针一样直接操作内存,因为Java中CAS操作的执行都依赖于Unsafe类的方法。

注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源来执行相应的任务。

CAS是一条CPU并发原语,它的功能是判断内存某个位置的值是否是预期值,如果是预期值则改为新的值,否则不做修改,并且这个过程是原子的。

CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能!通过它来实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,原语的执行必须是连续的,在执行的过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

举个例子,java.util.concurrent.atomic包下面有一个原子整型类AtomicInteger(AtomicInteger这个类的存在是为了满足在高并发的情况下,原生的整形数值自增线程不安全的问题)AtomicInteger类里面有一个方法getAndIncrement()如下( getAndIncrement()方法的作用就是先获取值再加1):

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

这个方法中就是用到了Unsafe类中的getAndAddInt方法了,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;
    }

我们可以看到getAndAddInt方法中并没有出现synchronized关键字,也就是没有加锁。方法进入首先定义一个变量var5,然后执行do里面的语句,var5 = this.getIntVolatile(var1, var2); var1就是this,var2就是valueOffset即内存地址偏移量,getIntVolatile(var1, var2)的作用就是获取当前对象this在valueOffset这个地址偏移量上的值是多少,然后赋值给var5,this.compareAndSwapInt(var1, var2, var5, var5 + var4)这句话的意思是比较当前对象的值是不是和var5相等,如果等于就返回true,取反之后退出do-while循环。简单来说:

  • var1就是AtomicInteger对象本身;
  • var2该对象值的引用地址;
  • var4需要变动(增加或者减少)的数量;
  • var5是通过var1和var2找出的主内存中真实值;
  • 然后使用该对象的当前值与var5比较——如果相同更新var5+var4并且返回true;不同继续取值比较,直到更新完成。

假设线程A和线程B两个线程同时执行getAndAddInt操作:

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别在各自的工作内存中。
  2. 线程A首先通过getIntVolatile(var1, var2)拿到了value值3,但是恰巧在此时线程A被挂起了
  3. 此时线程B也通过getIntVolatile(var1, var2)方法拿到value值3,但是线程B没有被挂起而是继续执行compareAndSwapInt方法,比较内存值也为3,成功修改内存值为4,然后线程B就执行完收工了,一切没有什么问题。
  4. 这时候线程A恢复,接着去执行compareAndSwapInt方法去比较,发现自己手里的值数字3和主内存中的值4不一致,说明该值已经被其他线程(就是线程B啦)抢先一步修改过了,那A线程本次修改就失败了,只能重新读取重新再来一遍。
  5. 线程A重新获取value值,因为变量value是被volatile修饰的,所以其他线程对它的修改线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较和替换,直到成功。

Java工程师面试1000题225-CAS原理解析_第1张图片

由于涉及到底层汇编指令,了解即可。

总结:CAS:CompareAndSwap,即比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。CAS中有三个操作数:内存值V,旧的预期值A,要修改的更新值B,当且仅当预期值A和内存值V相同时,才将内存值V修改为B,否则什么都不做。

CAS缺点:

  1. 循环时间长开销大:由于存在do-while循环,如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大开销;
  2. 只能保证一个共享变量的原子操作:对于多个共享变量操作时,循环CAS就无法保证操作的原子性了,这个时候只能使用锁来保证原子性了。
  3. 引出了ABA问题:下篇博客再讲吧。。。

你可能感兴趣的:(Java面试1000题)