CAS

张贤 2020年03月15日 105次浏览

Java 中的synchronized是一种悲观锁,悲观锁始终假设会发生并发冲突,因此会阻止一切可能违反数据完整性的操作。而 CAS 是一种乐观锁
CAS 全称是 Compare and Swap。乐观锁假设不会发生并发冲突,因此只在提交的时候检查是否违反数据完整性,如果提交失败则会进行重试。

CAS 支持原子更新操作,适用于计数器,序列发生器等场景。CAS 操作失败时由开发者决定是继续尝试,还是执行别的操作。

CAS 的思想:
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

public class CASCase {
    public volatile int value;

    public void add(){
        value++;
    }
}

使用javap查看编译后的add()方法中的value++的字节码如下:


2: getfield      #2                  // Field value:I
5: iconst_1
6: iadd
7: putfield      #2                  // Field value:I

可以看到value++被分成了三步,虽然value变量使用了volatile修饰,对value++操作禁止了指令重排序,但是value++不是原子性操作,在多线程并发下还是会产生线程安全问题。一种解决的办法是给add()方法加上synchronized关键字,但是synchronized属于悲观锁,是否能够进一步提升性能呢?其实还可以使用AtomicInteger来满足我们的要求

public class CASCase {
    public AtomicInteger value;

    public void add(){
        value.getAndIncrement();
    }
}

getAndIncrement()方法底层调用的是Unsafe类的compareAndSetInt方法,用到的就是 CAS 的思想。

CAS 多数情况下对开发者来说是透明的。J.U.C 的 atomic 包提供了常用了原子性数据类型以及引用、
数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。Unsafe 类虽然提供 CAS 服务,但是因为能够操纵任意内存地址读写而有隐患。在 Java9 之后,可以使用 Variable Handle API 来替代 Unsafe。

CAS 虽然很高效,但是也有缺点:

  • CAS 若循环时间长,则开销很大。我们可以看到在Unsafe类的getAndAddInt()执行时,如果 CAS 失败,会一直循环尝试进行 CAS 赋值,如果长时间都在循环,会给 CPU 带来很大开销
  • CAS 只能保证一个共享变量的原子操作,但无法保证对多个共享变量的原子性操作
  • ABA 问题。如果一个变量在进行 CAS 操作第一次读取时是 A,中间被其他操作改成了 B,又改回了 A,等到 CAS 操作第二次读取的时候也是 A,CAS 就会误认为这个变量从来没有被改变过。在一些场景中,ABA 问题可能会影响程序的并发正确性。为了解决这个问题,J.U.C 提供了AtomicStampedReference,可以通过变量值的版本来保证 CAS 的正确性。不过在 ABA 问题下,可能用传统的 加锁互斥的方法更加高效。