CAS原理

最后更新于:2022-08-13 12:49:42

原理

一种算法,一种CPU的特殊指令,这些特殊指令,由CPU保证了他们的原子性,其次,一个指令可以做多件事情,比如先比较,再更新。由于CPU保证了原子性,所以不会出现线程安全问题。

使用场景

并发

我认为V的值应该是A,如果是的话那我就把它改成B,如果不是A(说明被别人修改过了),那我就不修改了,避免多人同时修改导致出错。

等价代码

public class SimulatedCAS {
    private volatile int value;
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        // 1. 先比较
        if (oldValue == expectedValue) {
            value = newValue;
        }
        return oldValue;
    } 
}

案例演示

两个线程竞争,其中一个落败

两个人邀请另一个人吃饭,同时只能一个人邀请成功,谁先邀请,谁成功的机率就大一些。

public class TwoThreadsCompetition extends Thread {

    private volatile int value;

    public synchronized int compareAndSwap(int expectedValue, int newValue) {

        int oldValue = value;
        // 1. 先比较
        if (oldValue == expectedValue) {
            value = newValue;
            System.out.println(Thread.currentThread().getName()+"修改成功");
        }
        return oldValue;
    }

    public static void main(String[] args) throws InterruptedException {
        TwoThreadsCompetition r = new TwoThreadsCompetition();
        r.value = 0;
        Runnable target;
        Thread t1 = new Thread(r, "Thread-1");
        Thread t2 = new Thread(r, "Thread-2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(r.value);
    }

    @Override
    public void run() {
        compareAndSwap(0, 1);
    }
}

结果

Thread-1修改成功
1

场景一

  1. 乐观锁

    ConcurrentHashMap 源码中很典型的cas使用

  1. 并发容器
  2. 原子类

    AtomicInteger加载Unsafe工具,用来直接操作内存数据,使用Unsafe来实现底层操作
    用volatile修饰value字段,保证可见性

在静态代码块中引用value,同时value使用volatile修饰,保证可见性
在AtomicInteger数据定义的部分,我们还获取了unsafe实例,并且定义了valueOffset。再看到static块,懂类加载过程的都知道,static块的加载发生于类加载的时候,是最先初始化的,这时候我们调用unsafe的objectFieldOffset从Atomic类文件中获取value的偏移量,那么valueOffset其实就是记录value的偏移量的。
valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,这样我们就能通过unsafe来实现CAS了。value是用volatile修饰的,保证了多线程之间看到的value值是同一份。

我们看var5获取的是什么,通过调用unsafe的getIntVolatile(var1, var2),这是个native方法,其实就是获取var1中,var2偏移量处的值。var1就是AtomicInteger,var2就是我们前面提到的valueOffset,这样我们就从内存里获取到现在valueOffset处的值了。

现在重点来了,compareAndSwapInt(var1, var2, var5, var5 + var4)其实换成compareAndSwapInt(obj, offset, expect, update)比较清楚,意思就是如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果这一步的CAS没有成功,那就采用自旋的方式继续进行CAS操作。

Unsafe的getAndAddInt方法分析:自旋 + CAS,在这个过程中,通过compareAndSwapInt比较并更新value值,如果更新失败,重新获取,然后再次更新,直到更新成功。

Unsafe类

Unsafe是CAS的核心类。Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。

缺点

  1. ABA问题

CAS只是判断值是否相等,而不是判断是否修改过,如:一个线程拿到一个值为1,然后进行计算,在这个过程中,来了一个线程,把1改成了2,紧接着又一个线程把数据改成1,而这时第一个线程,计算结束,判断值仍然为1,就认为数据在此期间没有被修改过,然后将值修改为对应的结果。而实际上该数据已经被修改多次了。

解决方法:添加版本号,每次通过对比版本号。而不是通过对比值

  1. 自旋时间过长

在=源码中可以看到,有些地方是进行死循环的,而再次期间如果竞争非常激烈,或者锁一直拿不到,那么对内存是很大的消耗