理解CAS

2020年8月1日
理解CAS插图

本文出自明月工作室:https://www.freebytes.net/it/java/learn-cas.html

简介

众所周知,synchronize能够保证数据操作的原子性,从而实现同步效果。但是这种通过多个线程竞争锁的方式其效率会比较差,于是java另外实现了另一种无锁的却能保证数据操作原子性的方式——CAS(Compare-and-swap)。

CAS的原理

Compare-and-swap,是一个针对非抢占式微处理器的一条指定指令的宽泛术语,这条指令读取内存的位置,比较独到的值和期望的值,当读到的值等于期望的值时,就将新值写入到该内存位置;否则,不做理会。

说的通俗点就是,线程在修改一个变量值时,首先判断这个变量有没有被修改过,如果没有,那就执行修改。

比如:多个线程同时修改一个值(count=0)时,每个线程的工作内存中都有这个值的副本(count=0),当线程开始对其进行修改时,先把0作为一个期望值,与主内存中count的实际值相比较,如果主内存中的值确实等于期望值,那么才可以进行对这个值的修改,并将修改后的值再次写入主内存中,这便是一次比较并交换的过程(注意:这个过程是原子性的,下文会提到);然而如果期望值不等于实际值,该线程需要重新从主内存中获取count的值,以便用于下一次的比较交换操作。

案例

下面通过实际例子加深理解——

现在有10000个线程对变量int i = 0进行i++操作,如果不加任何同步,那么有可能最终打印出的i的值是小于10000的数字。

public class Test {

    static int count = 0;

    public static void main(String[] argv) {
        for (int i = 0; i < 10000; i++) {
            TestThread thread = new TestThread();
            thread.start();
        }
        while (Thread.activeCount()>1){
        }
        System.out.println(count);
    }
}

class TestThread extends Thread {
    @Override
    public void run() {
        Test.count++;
    }
}

输出:
9999

最简单的解决方法就是给线程类的Test.count++语句加上synchronize锁,但CAS是如何解决的呢?

这是一个以CAS作为底层的原子类AtomicInteger的应用案例——

public class TestCAS {
    static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            TestCASThread thread = new TestCASThread();
            thread.start();
        }
        while (Thread.activeCount()>1){
        }
        System.out.println(count.get());
    }
}

class TestCASThread extends Thread {
    @Override
    public void run() {
        TestCAS.count.incrementAndGet();
    }
}

输出:
10000

incrementAndGet()方法,是递增1的意思。它的底层调用的是Unsafe类的compareAndSwapInt()方法——

    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;
    }

由于这个Unsafe类没有源码,所以只能看到class文件的反编译代码,挺难看的,但能够看出,它在循环访问compareAndSwapInt()方法,进行重复的比较交换,比较交换成功之后才返回值。

compareAndSwapInt() 方法以原子的方式执行下列指令的序列:

//获取内存中的实际值
int readValue = value;
//将期望值与实际值进行比较,如果相同,就将其修改
if( readValue == expectedValue ){
   value = newValue;
}
//期望值与实际值不同,证明该值已被其他线程修改,返回实际值
return readValue;

也就是说,CAS的读-改-写序列,是一个原子性操作,也只有这样,才能保证线程在对变量进行比较交换的过程中,不会出现脏读的情况。

CAS的缺陷

缺陷一:cas身为一种乐观锁,只能适用在并发数不算高的场景。因为cas的一个特点就是,在一个线程对变量进行比较并交换的操作时,其他线程都在循环获取变量最新的值,那么当并发数太大的时候,会对cpu造成太高的消耗。

缺陷二:ABA问题。假设变量i的初始值为A, 两个线程同时修改i,只有当i的期望值等于A时,才能成功。这种情况下,通常只有一个线程能够修改成功。但是存在这样一种特殊情况——
1、线程1希望将变量i从A修改为B,线程2希望将变量i从A修改为C。
2、线程1先一步将i的值从A修改为了B。
3、这时有另外的线程3一次将i的值从B修改为了A。
4、线程2获取i的值,发现跟期望值A一致,于是将变量i从A修改为了C。

最后一步,当线程2获取到i的实际值时,虽然与期望值一致,但是其实i的值已经不是原来的A了,而是经过2次变换之后的A,此A非彼A。

如果只是将cas用在简单的数据计算上,那么即时有ABA问题,也不影响数据的正确结果。但是在某些场景下,是会出大问题的。要解决这个ABA的问题,可以使用java中的另一个原子类AtomicStampedReference。

这个类的实现思想是,不再单纯地把数据的实际值当做用于比较的值,而是在这基础上增加了版本号(时间戳)的概念,每一次值的改变,都要更新版本号的信息,然后在每一次的比较并交换过程中,比较的不仅是对象的实际值,还有值的版本号。