并发知识-- CAS

本文最后更新于:4 个月前

CAS (Compare And Swap) (CompareAndSet): 比较并交换,比较并设定

CAS 是一种并发时用到的技术,其本质是一种算法

为了解决并发时修改共享变量的问题,传统的解决方式就是加锁,例如典型的 synchronized 关键字,但是加锁一般都是悲观锁,即首先认为每次都会进行多线程的竞争,所以首先加锁,其他线程需要阻塞直到锁释放

但是加锁可能会带来额外的性能开销,如果是对于轻量级的并发,或者只是线程交替执行,就可能显得很笨重

那么,乐观锁应运而生,而它的一个实现就是 CAS

乐观锁是首先认为没有其他线程会对同一个数据进行改变,而当真正去操作共享变量的时候,再去看共享变量有没有被更新,如果没有就进行一个原子性的业务操作(通过硬件支持将非原子性的操作变成原子性),反之操作失败

CAS 的思想当然也和上面的类似,只是具体实现细节需要我们探究

Java 中实现 CAS 的是 Unsafe 类,其中有一个本地方法

compareAndSwapObject(Object var1, long v2, Object v4, Object v5);

其中 var1 就是要改变的类对象,v2 则是要修改的字段的内存地址,v4 则是未修改之前的预期值,v5 则是新的值

这个方法是通过 JNI 调用 Unsafe.cpp 文件的一个函数,而这个函数(在Linux 版本下)又是汇编语言的cmpxchg指令,所以实际上是硬件层面上提供的 API 实现的

CAS 产生的问题:

  1. 只能对单一共享变量操作

    如果需要对多个变量进行 CAS 同步更新,那么还是得加锁解决

  2. ABA

    如果一个线程 CAS 将 A 值改变成了 B,另一个线程再将 B 改变成了 A,然后又有一个线程看到的依然是 A,就会认为这个值一直是 A,没有改变,此时在将 A 改为 C,这样就覆盖了之前两个线程的操作

    • 解决办法是: 增加一个字段来标识整个对象的版本,一旦进行了修改,就改变版本号

      JDK 的实现: AtomicStampedReference

      将对象和一个版本戳组成一个内部类 pair,相比没有版本戳的例如 AtomicInteger 每次更新还需要传入预期的版本戳

  3. 如果用到了 CAS + 自旋

    如果 CAS 不成功,会一直自旋重试导致 CPU 占用资源

    甚至类似产生死锁的问题,那么就会一直占用,更损耗性能

    所以要根据情况选择是否要加入自旋,分场景考虑