当我谈CAS时,我谈些什么

当你学Java的并发包JUC时,不可避免,会经常遇见乐观锁、自旋锁、CAS等字眼,实际上它们都是对同一个技术不同层面的一些描述。volatile变量的读写和CAS可以实现线程之间的通信,这是JUC包得以实现的基石,我们必须得对CAS有所了解。

什么是CAS

对于并发控制而言,锁是一种悲观的策略,它总是假设每一次的临界区操作会产生冲突,产生冲突的线程会被挂起等待;无锁则是一种乐观的策略,它假设对临界区的操作是没有冲突的,线程不需要被挂起,但当遇到冲突时,可以使用CAS技术来解决,从而可以避免加锁,从而减少上下文的切换。

CAS,全称是Compare And Swap,其核心思想是:它包含三个参数CAS(V, E, N),V表示需要被更新的变量,E表示期望V应该有的值,N表示V的新值。当且仅当V值等于E值时,才会将V值设为N值,若V值和E值不同,说明有其它线程对V做了更新,则当前线程什么都不做。当多个线程同时使用CAS操作一个变量时,只有一个会胜出并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试。

CAS在Java中的实现

目前,在硬件层面已支持原子化的CAS指令(例如x86指令集的cmpxchg指令),即将比较和交换这两个操作封装为一个原子指令。

在JDK 5之后,Java类库中才开始使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。HotSpot虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件内联进去。

不过,需要注意的是,用户并不能直接使用Unsafe类,Unsafe::getUnsafe()的代码中限制了只有启动类加载器(BootstrapClassLoader)加载的Class才能访问它,因此只有Java类库可是使用Unsafe的CAS操作,用户如果有使用CAS的需求,只能通过Java类库的API来间接使用它。

CAS的缺点

CAS存在三大问题:循环时间长开销大,只能保证一个共享变量的原子操作,以及ABA问题。

  • 循环时间开销大

JUC包中对CAS的使用基本是基于自旋的方式,即当预期值和主内存中的值不等时,就重新获取主内存中的值,再次尝试CAS,直到成功为止,这就是自旋,例如下面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;
    }

正因为是通过自旋实现,如果长时间不成功,会给CPU带来很大的开销。

  • 只能保证一个共享变量的原子操作。当对多个变量进行操作时,就无法保证原子性,必须要用锁。当然,可以用取巧的方法,把多个变量封装到一个类中,然后用AtomicReference来保证引用对象之间的原子性。
  • ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

每日学习笔记,写于2020-04-30 星期四

你可能感兴趣的:(当我谈CAS时,我谈些什么)