JAVA 中 CAS 原理详解

什么是 CAS

CAS, Compare and Swap 即比较并替换, CAS 有三个操作数:内存值 V、旧的预期值 A、要修改的值 B, 当且仅当预期值 A 和内存值 V 相同时, 将内存值修改为 B 并返回 true, 否则什么都不做并返回 false。

java.util.concurrent.atomic 包下的原子操作类都是基于 CAS 实现的, 接下去我们通过 AtomicInteger 来看看是如何通过 CAS 实现原子操作的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AtomicInteger extends Number implements java.io.Serializable {
// 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;
public final int get() {return value;}
}
  1. Unsafe 是 CAS 的核心类, Java 无法直接访问底层操作系统, 而是通过本地(native)方法来访问。不过尽管如此, JVM 还是开了一个后门, JDK 中有一个类 Unsafe, 它提供了硬件级别的原子操作。

  2. valueOffset 表示的是变量值在内存中的偏移地址, 因为 Unsafe 就是根据内存偏移地址获取数据的原值的。

  3. value 是用 volatile 修饰的, 保证了多线程之间看到的 value 值是同一份。

接下去, 我们看看 AtomicInteger 是如何实现并发下的累加操作:

1
2
3
4
5
6
7
8
9
10
11
12
//jdk1.8实现
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}

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;
}

假设现在线程 A 和线程 B 同时执行 getAndAdd 操作:

  1. AtomicInteger 里面的 value 原始值为 3, 即主内存中 AtomicInteger 的 value 为 3, 根据 Java 内存模型, 线程 A 和线程 B 各自持有一份 value 的副本, 值为 3。

  2. 线程 A 通过 getIntVolatile(var1, var2) 方法获取到 value 值 3, 线程切换, 线程 A 挂起。

  3. 线程 B 通过 getIntVolatile(var1, var2) 方法获取到 value 值 3, 并利用 compareAndSwapInt 方法比较内存值也为 3, 比较成功, 修改内存值为 2, 线程切换, 线程 B 挂起。

  4. 线程 A 恢复, 利用 compareAndSwapInt 方法比较, 发手里的值 3 和内存值 2 不一致, 此时 value 正在被另外一个线程修改, 线程 A 不能修改 value 值

  5. 线程的 compareAndSwapInt 实现, 循环判断, 重新获取 value 值, 因为 value 是 volatile 变量, 所以线程对它的修改, 线程 A 总是能够看到。线程 A 继续利用
    compareAndSwapInt 进行比较并替换, 直到 compareAndSwapInt 修改成功返回 true。

整个过程中, 利用 CAS 保证了对于 value 的修改的线程安全性。

CAS 缺点

ABA 问题

如果变量 V 初次读取的时候是 A, 并且在准备赋值的时候检查到它仍然是 A, 那能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了 B, 然后又改回 A, 那 CAS 操作就会误认为它从来没有被修改过。针对这种情况, java 并发包中提供了一个带有标记的原子引用类 “AtomicStampedReference”, 它可以通过控制变量值的版本来保证 CAS 的正确性。

循环时间长开销大

自旋 CAS 如果长时间不成功, 会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升, pause 指令有两个作用, 第一它可以延迟流水线执行指令(de-pipeline), 使 CPU 不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本, 在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush), 从而提高 CPU 的执行效率。

只能保证一个共享变量的原子操作

当对一个共享变量执行操作时, 我们可以使用循环 CAS 的方式来保证原子操作, 但是对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候就可以用锁, 或者有一个取巧的办法, 就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i=2,j=a, 合并一下 ij=2a, 然后用 CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性, 你可以把多个变量放在一个对象里来进行 CAS 操作。