什么是 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
汇编指令, 该指定为 原子指令