CAS 是什么

2,871 阅读7分钟

CAS (Compare And Swap) 比较并交换

CAS 是一条CPU并发原语。功能是判断内存某个位置的值是否为预期值,如果是则更改为最新值,这个过程是原子的。CAS并发原语体现在Java语言中就是sun.misc.Unsafe类中的各个本地方法。这是一种完全依赖于硬件的功能,通过它实现了原子操作。原语的执行时连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致。

CAS的作用是比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。 如果单看这句话不知道在说什么,那么就继续往下看,通篇结束之后再看读一下这句话吧。

这一篇通过AtomicInteger中两个方法来探索CAS到底是如何实现的。

AtomicInteger的两个方法

compareAndSet

先来看三行代码

AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,10)+ "\t current data : " +atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,9)+ "\t current data : " +atomicInteger.get()); 

上篇文章对volatile的理解 提到了,可以使用原子类AtomicInteger 解决原子性问题。这里用到了AtomicInteger.compareAndSet方法,两个参数分别是期望值更新值,如果期望值与此时内存中的值相同,则将内存中的值更新为更新值,方法返回值为布尔类型,表示是否更新成功。以上三行代码,初始化时设置内存中值为5,第二行期望值与更新值分别为5和10,如果期望值5与内存中的值5相同,则将内存中的值更新为10,第二行返回true,当前内存中的值为10;第三行期望值为5,当前内存中的值为10,不符合期望结果,则不更新内存中的结果,返回false,当前内存中的值未更改,还是10。代码输出结果如下:

true	 current data : 10
false	 current data : 10

概述一下,CAS涉及到3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

getAndIncrement

我们知道在多线程情况下i++操作其实包含了三个步骤,是线程不安全的。我们可以使用atomicInteger.getAndIncrement() 实现线程安全的i++,下面通过看下该方法的实现了解如何做到线程安全。

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

以上是getAndIncrement的源代码,注释在当前值的基础上原子增加一。实现为调用unsafe类的getAndAddInt方法,该方法有三个参数,分别是当前对象、内存偏移量、1,再往下看,就进入到unsafe类。

  1. unsafe类
    unsafe类是CAS的核心类。java是无法直接访问底层系统的,需要通过本地(native)方法来访问,Unsafe的所有方法都是native来修饰的,也就是说Unsafe类中的基础方法都直接调用操作系统底层资源执行相应任务,即可以像C的指针一样直接访问内存。
  2. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。以下代码给出了valueOffset的来源
private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
  1. 变量value用volatile修饰,保证了多线程之间内存可见性。因此拿到的内存地址总是最新的。

了解这几个概念,我们继续往下看,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;
    }

这仍然是存在于unfase的方法,var1对应this,即当前对象,var2为内存偏移量,var4即是要添加的“1”。
当前方法为do-while结构,根据方法名中getAndAdd,首先get操作,do结构首先根据对象与内存地址获取当前值var5,从物理主内存中拷贝到自己的线程工作内存;然后关键点来了:while里面判断如果这一时刻内存地址中的值与var5相同,则修改为var5+var4,即Add成功,这时返回true,while取反,条件不成立,不再执行do;如果刚拿到var5的值,主物理内存中的值就发生变化了,即主内存与var5的值不一致,则不修改,返回false,while条件满足,继续执行do操作,再次获取当前内存中最新的值,继续判断,直至保证加一时拿到的结果时最新的。
以上操作简单说,就是拿到var5,然后对var5+1,担心加一操作时var5的值已经发生变化,所以加一时要再验证一遍,如果这个内存中的值还是var5那么就放心的加一操作,否则就不停的去获取最新的值。

下面举个例子来具体解释上一段内容:

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

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger中的value为3,根据JMM模型(参考上篇文章对volatile的理解),线程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不一致,说明该值已经被其他线程抢先修改过了,那么A线程本次修改失败,重新读取最新值重来一遍,即重新执行do操作。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对他的修改,线程A总能第一时间看到,线程A执行compareAndSwapInt进行比较替换,直至成功。

再概述一下,CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 这时再去看开篇的那句话是否就理解了呢。

CAS的缺点

  1. 循环时间长开销很大
    • 根据源码可以看到,内部采用do-while结构,如果while条件不满足则一直尝试获取新的值,由于CAS没有锁,可能有多个线程同时来修改内存中的值,当多个线程同时在do代码块中不断循环时,给CPU带来很大的开销。而synchronized则保证同一时间只有一个线程在真正修改内存中的值。
  2. 只能保证一个共享变量的原子操作
    • 由于getAndAddInt方法中的第一个参数是object,即this 表示当前对象,从数量上来说这里只能控制一个共享变量。而synchronized加锁能锁住一段代码,可以保证多个线程都被锁住。
  3. 引出来ABA问题
    • ABA问题:线程A要将5修改为10,现在get到内存中的值为5,这是被挂起;线程B将内存中的值从5改为6,然后又从6改为5,B的一顿操作过后A获取资源继续进入while判断,发现内存中的值还是5跟自己手里持有的值相同,可以进行更新操作将5更为10。在A看来get和set操作中间没有发生变化,get的是5,set的时候还是5,实际上里面的值已经发生过变化了。尽管线程A的CAS操作成功,但是不代表这个过程就没有问题。