CAS详解¶
CAS(Compare And Swap,比较并交换)是实现乐观锁和无锁并发的底层原子操作,本质是一条“先比较再更新”的 CPU 原子指令,在 Java 中通过 Unsafe 和原子类对外暴露。
核心思想与三大参数
CAS 操作包含三个值: + V:内存中当前值(variable)。 + E:期望值(expected)。 + N:新值(new)。
执行过程:如果当前内存值 V 等于期望值 E,则把 V 更新为 N,整个比较+更新过程是一个不可分割的原子操作;如果 V ≠ E,则不做任何修改,返回失败,调用方可以选择重试或放弃。
你可以把它想象成:“如果现在冰箱里还是我刚刚看到的那瓶饮料(E),那我就把它拿走(更新为 N);如果已经不是那个状态了,什么都不做,再想别的办法。”
Java 中 CAS 的实现位置¶
在 Java 中,CAS 的底层实现依赖于 JVM 提供的“魔法类” Unsafe,它最终调用 CPU 的 cmpxchg 等原子指令来完成操作。
Unsafe 中典型的 CAS 方法(简化):
boolean compareAndSwapInt(Object o, long offset, int expected, int x);
boolean compareAndSwapLong(Object o, long offset, long expected, long x);
boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
JUC 里的原子类(如 AtomicInteger)内部就是基于这些方法实现的 compareAndSet、getAndIncrement 等 API。
示例:AtomicInteger 自增的典型实现逻辑(简化为伪代码):
public class MyAtomicInteger {
private volatile int value;
public final int getAndAdd(int delta) {
for (;;) {
int current = get(); // 1. 读当前值
int next = current + delta; // 2. 计算新值
// 3. CAS 尝试更新,如果失败说明有并发修改,继续自旋
if (compareAndSet(current, next)) {
return current;
}
}
}
public final boolean compareAndSet(int expect, int update) {
// 内部调用 Unsafe#compareAndSwapInt
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int get() {
return value;
}
}
关键点:
- value 用 volatile 修饰,保证可见性(CAS 只保证原子性,不保证写后立刻对所有线程可见)。
- 通过 for(;;) 或 do-while 自旋重试来保证最终更新成功。
CAS 与乐观锁、自旋的关系¶
CAS 是乐观锁思想的典型实现:假设冲突不多,不加互斥锁,而是在提交前检查值是否被改动,如果改动了就重试。
典型模式就是“自旋”:
while (true) {
int oldValue = atomicInt.get();
int newValue = oldValue + 1;
if (atomicInt.compareAndSet(oldValue, newValue)) {
break; // 成功
}
// 失败就继续循环,相当于自旋锁
}
这种方式没有阻塞、挂起和恢复线程的开销,适合临界区很短、冲突概率不太高的高并发场景。
CAS 的优点
- 非阻塞:不需要像 synchronized 一样阻塞线程,没有锁竞争导致的上下文切换开销,通常性能更好。
- 原子性有硬件保证:基于 CPU 指令(如 cmpxchg),天生原子,不需要额外的软件锁来保护。
- 支撑高性能并发库:AQS、原子类、部分并发容器、LongAdder 等大量用到 CAS。
在 Java 项目中,典型用法包括:并发计数器、无锁队列、状态标记位(如一次性初始化标志)等。
CAS 的问题和局限¶
- ABA 问题 问题描述:如果一个变量从 A -> B -> A,被 CAS 的线程只看到“还是 A”,就会误以为期间没有发生过修改。
示例:
- 线程 T1 读取 V=A,准备把它改为 C。
- 线程 T2 把 V 从 A 改成 B,又改回 A。
- T1 执行 CAS,发现 V 仍然是 A,于是 CAS 成功,但实际期间数据经历过中间状态 B。
解决方案:
- 使用带“版本号/时间戳”的 CAS,如 AtomicStampedReference(值 + stamp 一起比较)。
-
或在业务层面通过版本号字段、乐观锁机制规避。
-
自旋开销过大
CAS 失败会继续自旋重试,如果竞争激烈,自旋次数可能很多,导致 CPU 空转严重。
因此:
- 在冲突很频繁、临界区较大或操作复杂时,更适合用悲观锁(ReentrantLock、synchronized)。
-
一些高级实现会采用“自旋 + 退避策略”(比如随机等待一小段时间再重试)。
-
只能保证单个变量原子性
CAS 天然只对一个内存位置 V 生效,对多个变量的复合操作无法直接保证原子性。
常见解决方式:
- 把多个字段封装进一个不可变对象,用 AtomicReference 进行 CAS(整体替换)。
- 或者退回到加锁(ReentrantLock/synchronized)方案来保证更大范围的原子性。
在 Java 中如何选用 CAS / 原子类¶
实际项目里,你可以按这几个原则来用:
- 简单计数或状态位(如并发访问计数、标志位):优先 AtomicInteger、AtomicBoolean 等,无需自己写 Unsafe。
- 高并发累加场景(如统计 QPS):优先 LongAdder/LongAccumulator,内部也是基于 CAS 做分段计数,减少热点竞争。
- 需要操作多个值的一致性:考虑 AtomicReference 封装对象 + CAS,或直接加锁。
- 复杂业务逻辑且冲突多:尽量用互斥锁或悲观锁(包括数据库锁),避免长时间自旋。