CAS 到底是嘛玩意

什么是 CAS ?

CAS: (Compare And Swap) 比较和交换, Java 就是通过 CAS 来实现原子操作的 !

如何使用 CAS ?

CAS 需要三个操作数, 分别是 : 内存地址, 旧的预期值, 准备设置的新值

CAS执行时, 对于一个变量 V, 他的旧的预期值为 A, 将要修改的值为 B, 有且仅当 V 符合 A, 处理器才会将 A 换成 B, 否者操作失败.

在 JDK 5之后, Java 类库才开始使用 CAS 操作, 该操作被封装在 sun.misc.Unsafe 类中, 如果你用过 java.util.concurrent.atomic 包中的类, 那么你一定对它不陌生, 所有的原子操作类, 底部调用的都是 Unsafe 的API

由于 Unsafe 并没有开源, 但是可以使用 IDEA 进行反编译看到源码 :

// UnSafe 类提供的方法, 用于 CAS 操作, 传入三个参数, 
// 依次为: 要操作的对象, 旧的预期值, 和需要设置的新值
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 获取 var1 的最新值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

从源码中可以看出, 采用的是循环 CAS, 需要先获取变量的最新值, 如果这个最新值等于旧的预期值, 那就将其交换并成功返回

CAS 存在的问题

CAS可以有效的提升并发的效率,但同时也会引入一些问题

  • ABA 问题

如果变量的初值为 A, 将要修改之前检查的值仍然为 A , 就能说明这个变量没有被其他线程改变过吗?
有可能在这个过程中, 其他线程将 A 换成 B, 但最后又被改为 A, 那么CAS操作就会认为这个值从来没有改变过. 这就是 ABA 问题(大部分情况下ABA问题不会影响程序并发的正确性)

  • 消耗 CPU

如果线程竞争激烈, 有可能存在一些线程长时间无法操作成功, 会给 CPU 带来非常大的开销

  • 只能操作单一变量

对于多个共享变量操作时, 一次 CAS 就无法保证其原子性了

如何解决以上问题

ABA 问题可以为变量添加一个版本号或者时间戳来保证 CAS 的正确性, java.util.concurrent 包提供了一个带有标记的原子引用类 AtomicStampedReference
不过这个类处于比较鸡肋的位置, 大部分情况下 ABA 问题并不会影响程序并发的正确性

对于消耗 CPU 的问题, 如果 JVM 支持 CPU 的 pause 指令, 那么效率会有一定的提升

如何解决同时操作单一变量的问题, 可以将这些变量封装在一个对象中, 使用原引用类 AtomicReference 来保证原子性, 这样就能同时操作多个变量啦 !

CAS 的底层实现

使用 java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 可以反汇编生成汇编指令 (需要安装 HSDIS 插件) , 通过查看 HotSpot 的 C/C++ 源码, 也能查看其底层汇编实现

  • 在 IA64, x86 架构中用 cmpxchg 来实现 CAS
  • 在 SPARC-TSO 中用 casa 指令实现
  • 在 ARM 和 PowerPC 架构下使用一对 ldrex/strex 指令完成 LL/SC 的功能

目前主流 CPU 就是 x86 架构的 Intel, 所有基本上都是用 cmpxchg 实现的 CAS, 但是这依然不是一个原子指令, 仍然需要加上 lock 前缀, 通过锁定 北桥电信号 (不采用锁总线) 实现原子指令, 所以, CAS 最终的底层实现是 lock cmpxchg 汇编指令, 该指定为 原子指令

你可能感兴趣的:(并发,java,并发编程)