CAS
多线程 CAS:无锁乐观锁,原子性保证,ABA 问题,自旋代价,AtomicInteger 实现详解。
CAS
一、什么是 CAS?
CAS 的全称是 Compare-And-Swap(比较并交换)。 它是并发编程中 乐观锁(Optimistic Locking)的核心实现方式。
它只做一件事:
“我认为内存里现在应该是数值 A,如果是,你就帮我把它改成数值 B;如果不是,那就告诉我失败了,我什么都不改。”
这整个“比较+修改”的过程,是 原子性 的(不可被打断)。
二、CAS 的三大要素
要执行 CAS,必须包含三个操作数:
- V (Value):要修改的 内存地址(内存中存的那个值)。
- E (Expected):我 期望 在这个地址看到的旧值(也就是我刚才读到的值)。
- N (New):我想把它修改成的 新值。
CAS 的执行逻辑公式:
只有当
V == E时,才把V更新为N。 否则,不做任何操作。
三、执行流程
假设内存地址 V 处存着数字 10。
现在有两个线程,线程 A 和线程 B,同时要对 V 的数字进行累加。

1. 初始状态
- 内存 V = 10。
2. 线程 A 进场
- 读取:线程 A 读到 V = 10。它决定要把 10 变成 11。
- 准备 CAS:期望值 E = 10,新值 N = 11。
- 执行 CAS:线程 A 拿 E(10) 去跟内存 V(10) 比。
- 相等吗?相等。
- 结果:线程 A 成功 将内存 V 改为 11。
3. 线程 B 进场(慢了一步)
- 读取:线程 B 实际上可能在 A 修改之前也读到了 10。它也决定要把 10 变成 11。
- 准备 CAS:期望值 E = 10,新值 N = 11。
- 执行 CAS:线程 B 拿 E(10) 去跟内存 V 比。
- 关键点:此时内存 V 已经被 A 改成了 11。
- 相等吗?
10 != 11,不相等。 - 结果:线程 B 失败,内存 V 保持 11 不变。
4. 失败后的处理(自旋)
- 线程 B 发现失败了,它不会报错,而是进入 自旋(Spin):
- 重新读取内存 V,这次读到 11。
- 重新计算:我要把它变成 12。
- 再次发起 CAS:期望 E = 11,新值 N = 12。
- 这次对比成功,修改为 12。
Important
⚠️ 一定注意: CAS 失败后就结束了, CAS 本身是没有自旋的,自旋是我们自己对 CAS 失败做的处理策略。
四、为什么 CAS 是原子的(底层原理)?
你可能会问:“比较和交换是两个动作啊,万一我比较完了,还没来得及交换,别人插队修改了怎么办?”
这就涉及到了操作系统和硬件层面:
- Java 层面 (
Unsafe类) Java 的 CAS 操作都是通过sun.misc.Unsafe类实现的(比如compareAndSwapInt方法)。这是一个这种直接操作内存的“后门”。 - 操作系统与 CPU 层面
Unsafe里的方法是个native方法,它最终会编译成一条 CPU 指令:cmpxchg(Compare and Exchange)。
- 在单核 CPU 上,一条指令天然是原子的。
- 在多核 CPU 上:操作系统会在
cmpxchg指令前加上lock前缀(lock cmpxchg ...)。这个前缀会锁定 CPU 的总线(或者锁定缓存行),确保在执行这一条指令的微秒级时间内,其他 CPU 核心不能访问这块内存。
结论:是 硬件指令 保证了“比较”和“交换”这两个动作合二为一,中间无法插入。
五、CAS 的三大缺陷
虽然 CAS 看起来很完美(无锁、高性能),但它有三个著名的坑:
1. ABA 问题(经典的狸猫换太子)
场景:
- 内存里原本是 A。
- 线程 1 读到 A,准备改成 B,但稍微顿了一下。
- 在此期间,线程 2 把 A 改成了 B,又把 B 改回了 A。
- 线程 1 恢复运行,去对比:“咦?内存里还是 A 嘛,没变过,那我就改了。” -> 成功。
后果: 虽然数值最终是对的,但在某些场景(比如栈结构、链表节点)中,这个“A”已经不是原来的“A”了,中间的状态丢失可能导致严重的数据错乱。
解决:
使用版本号,就像给数据打戳:1A -> 2B -> 3A。
Java 提供了 AtomicStampedReference 来解决这个问题(对比引用 + 版本号)。
2. 循环时间长开销大(自旋的代价)
如果并发极其剧烈,大量线程在 do-while 循环里 CAS 失败。
它们会不断地:读 -> 失败 -> 读 -> 失败...
这会让 CPU 使用率飙升(空转)。
解决:如果是这种极高并发场景,应该使用 LongAdder(分段处理),或者干脆使用悲观锁(synchronized)。
Important
⚠️ 再次说明一遍:CAS 本身是没有自旋的,自旋是我们自己对 CAS 失败做的处理策略。
3. 只能保证一个共享变量的原子操作
CAS 只能锁住一个地址。如果你想原子的同时修改 x 和 y 两个变量,普通的 CAS 做不到。
解决:
- 把
x和y封装成一个对象,用AtomicReference进行 CAS。 - 或者直接用
synchronized。
六、AtomicInteger 例子详解
让我们来看看 AtomicInteger 是如何通过 CAS 实现并发下的累加操作的,以 AtomicInteger 的 getAndAdd 方法为突破口。
getAndAdd 方法
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
可以看出,这里使用了 Unsafe 这个类,这里需要简要介绍一下 Unsafe 类:
Unsafe 类
Unsafe 是 CAS 的核心类。Java 无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM 还是开了一个后门,JDK 中有一个类 Unsafe,它提供了硬件级别的原子操作。
AtomicInteger 加载 Unsafe 工具,用来直接操作内存数据
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;}
}
在 AtomicInteger 数据定义的部分,我们还获取了 unsafe 实例,并且定义了 valueOffset。再看到 static 块,懂类加载过程的都知道,static 块的加载发生于类加载的时候,是最先初始化的,这时候我们调用 unsafe 的 objectFieldOffset 从 Atomic 类文件中获取 value 的偏移量,那么 valueOffset 其实就是记录 value 的偏移量的。
valueOffset 表示的是变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 unsafe 来实现 CAS 了。value 是用 volatile 修饰的,保证了多线程之间看到的 value 值是同一份。
Unsafe 的 getAndAddInt 方法的实现
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
我们看 o 获取的是什么,通过调用 unsafe 的 getIntVolatile(o, offset),这是个 native 方法,其实就是获取 o 中,offset 偏移量处的值。
o 就是 AtomicInteger,offset 就是我们前面提到的 valueOffset, 这样我们就从内存里获取到现在 valueOffset 处的值了。
现在重点来了,compareAndSwapInt(o, offset, delta, v + delta) 其实换成 compareAndSwapInt(obj, offset, expect, update) 比较清楚,意思就是如果 obj 内的 value 和 expect 相等,就证明没有其他线程改变过这个变量,那么就更新它为 update,如果这一步的 CAS 没有成功,那就采用自旋的方式继续进行 CAS 操作。
Unsafe 的 getAndAddInt 方法分析:自旋 + CAS,在这个过程中,通过 compareAndSwapInt 比较并更新 value 值,如果更新失败,重新获取,然后再次更新,直到更新成功。