[转]Java CAS 原理剖析

本贴最后更新于 1716 天前,其中的信息可能已经时异事殊

Java CAS 原理剖析

本文转载来自卡巴拉的树Java CAS 原理剖析

在 Java 并发中,我们最初接触的应该就是 synchronized 关键字了,但是 synchronized 属于重量级锁,很多时候会引起性能问题,volatile 也是个不错的选择,但是 volatile 不能保证原子性,只能在某些场合下使用。

synchronized 这种独占锁属于悲观锁,它是在假设一定会发生冲突的,那么加锁恰好有用,除此之外,还有乐观锁,乐观锁的含义就是假设没有发生冲突,那么我正好可以进行某项操作,如果要是发生冲突呢,那我就重试直到成功,乐观锁最常见的就是 CAS

我们在读 Concurrent 包下的类的源码时,发现无论是 ReenterLock 内部的 AQS,还是各种 Atomic 开头的原子类,内部都应用到了 CAS,最常见的就是我们在并发编程时遇到的 i++ 这种情况。传统的方法肯定是在方法上加上 synchronized 关键字:


public class Test {



    public volatile int i;



    public synchronized void add() {

        i++;

    }

}

但是这种方法在性能上可能会差一点,我们还可以使用 AtomicInteger,就可以保证 i 原子的 ++ 了。


public class Test {



    public AtomicInteger i;



    public void add() {

        i.getAndIncrement();

    }

}

我们来看 getAndIncrement 的内部:


public final int getAndIncrement() {

    return unsafe.getAndAddInt(this, valueOffset, 1);

}

再深入到 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;

}

这里我们见到 compareAndSwapInt 这个函数,它也是 CAS 缩写的由来。那么仔细分析下这个函数做了什么呢?

首先我们发现 compareAndSwapInt 前面的 this,那么它属于哪个类呢,我们看上一步 getAndAddInt,前面是 unsafe。这里我们进入的 Unsafe 类。这里要对 Unsafe 类做个说明。结合 AtomicInteger 的定义来说:


public class AtomicInteger extends Number implements java.io.Serializable {

    private static final long serialVersionUID = 6214790243416807050L;

    

    // setup to use Unsafe.compareAndSwapInt for updates

    private static final Unsafe unsafe = Unsafe.getUnsafe();

    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;

    ...

AtomicInteger 数据定义的部分,我们可以看到,其实实际存储的值是放在 value 中的,除此之外我们还获取了 unsafe 实例,并且定义了 valueOffset。再看到 static 块,懂类加载过程的都知道,static 块的加载发生于类加载的时候,是最先初始化的,这时候我们调用 unsafeobjectFieldOffsetAtomic 类文件中获取 value 的偏移量,那么 valueOffset 其实就是记录 value 的偏移量的。

再回到上面一个函数 getAndAddInt,我们看 var5 获取的是什么,通过调用 unsafegetIntVolatile(var1, var2),这是个 native 方法,具体实现到 JDK 源码里去看了,其实就是获取 var1 中,var2 偏移量处的值。var1 就是 AtomicIntegervar2 就是我们前面提到的 valueOffset,这样我们就从内存里获取到现在 valueOffset 处的值了。

现在重点来了,compareAndSwapInt(var1, var2, var5, var5 + var4) 其实换成 compareAndSwapInt(obj, offset, expect, update) 比较清楚,意思就是如果 obj 内的 valueexpect 相等,就证明没有其他线程改变过这个变量,那么就更新它为 update,如果这一步的 CAS 没有成功,那就采用自旋的方式继续进行 CAS 操作,取出乍一看这也是两个步骤了啊,其实在 JNI 里是借助于一个 CPU 指令完成的。所以还是原子操作。

1. CAS 底层原理

CAS 底层使用 JNI 调用 C 代码实现的,如果你有 Hotspot 源码,那么在 Unsafe.cpp 里可以找到它的实现:


static JNINativeMethod methods_15[] = {

    //省略一堆代码...

    {CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},

    {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z",      FN_PTR(Unsafe_CompareAndSwapLong)},

    //省略一堆代码...

};

我们可以看到 compareAndSwapInt 实现是在 Unsafe_CompareAndSwapInt 里面,再深入到 Unsafe_CompareAndSwapInt:


UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))

  UnsafeWrapper("Unsafe_CompareAndSwapInt");

  oop p = JNIHandles::resolve(obj);

  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);

  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

UNSAFE_END

p 是取出的对象,addr 是 p 中 offset 处的地址,最后调用了 Atomic::cmpxchg(x, addr, e), 其中参数 x 是即将更新的值,参数 e 是原内存的值。代码中能看到 cmpxchg 有基于各个平台的实现,这里我选择 Linux X86 平台下的源码分析:


inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {

  int mp = os::is_MP();

  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"

                    : "=a" (exchange_value)

                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)

                    : "cc", "memory");

  return exchange_value;

}

这是一段小汇编,__asm__ 说明是 ASM 汇编,__volatile__ 禁止编译器优化


// Adding a lock prefix to an instruction on MP machine

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

os::is_MP 判断当前系统是否为多核系统,如果是就给总线加锁,所以同一芯片上的其他处理器就暂时不能通过总线访问内存,保证了该指令在多处理器环境下的原子性。

在正式解读这段汇编前,我们来了解下嵌入汇编的基本格式:


asm ( assembler template

    : output operands                  /* optional */

    : input operands                   /* optional */

    : list of clobbered registers      /* optional */

    );

  • template 就是 cmpxchgl %1,(%3) 表示汇编模板

  • output operands 表示输出操作数,=a 对应 eax 寄存器

  • input operand 表示输入参数,%1 就是 exchange_value, %3dest, %4 就是 mpr 表示任意寄存器,a 还是 eax 寄存器

  • list of clobbered registers 就是些额外参数,cc 表示编译器 cmpxchgl 的执行将影响到标志寄存器, memory 告诉编译器要重新从内存中读取变量的最新值,这点实现了 volatile 的感觉。

那么表达式其实就是 cmpxchgl exchange_value ,dest,我们会发现 %2 也就是 compare_value 没有用上,这里就要分析 cmpxchgl 的语义了。cmpxchgl 末尾 l 表示操作数长度为 4,上面已经知道了。cmpxchgl 会默认比较 eax 寄存器的值即 compare_valueexchange_value 的值,如果相等,就把 dest 的值赋值给 exchange_value,否则,将 exchange_value 赋值给 eax。具体汇编指令可以查看 Intel 手册 CMPXCHG

最终,JDK 通过 CPU 的 cmpxchgl 指令的支持,实现 AtomicIntegerCAS 操作的原子性。

2. CAS 的问题

  1. ABA 问题

CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是 CAS 的 ABA 问题。 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A。 目前在 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. 循环时间长开销大

上面我们说过如果 CAS 不成功,则会原地自旋,如果长时间自旋会给 CPU 带来非常大的执行开销。

  • 并发
    75 引用 • 73 回帖 • 1 关注
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    324 引用 • 1395 回帖
  • CAS
    13 引用 • 21 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...