Java并发编程基石:深入理解CAS(Compare-And-Swap)
在Java并发编程中,如果你听过下面这些类:
- AtomicInteger / AtomicLong / AtomicReference
- ConcurrentHashMap(1.8+的部分实现)
- LongAdder / Striped64系列
- AQS中的状态更新
那么你其实已经大量间接使用过 CAS 了。
但很多人只是知道“CAS是无锁的”“比synchronized轻量”,却不清楚它到底是怎么实现的、有什么隐患、什么时候该用、什么时候坚决不能用。
这篇文章带你从原理 → 源码 → 优缺点 → 典型问题 → 工程实践 完整梳理一遍CAS。
一、CAS到底是什么?
CAS 全称:Compare And Swap(比较并交换)
核心思想只有一句话:
“我认为当前值应该是A,如果确实是A,就把它改为B;如果不是A,说明别人动过了,我不做修改(或者重试)。”
用伪代码表示就是:
boolean cas(地址V, 期望值A, 新值B) {
if (内存位置V的值 == A) {
把V的值改为B
return true;
} else {
return false;
}
}
关键点:上面整个“比较 → 修改”是一个原子操作,由CPU硬件指令直接保证(cmpxchg / cmpXchg / LL-SC 等)。
Java中并没有直接暴露这条指令,而是通过 sun.misc.Unsafe(后改为jdk.internal.misc.Unsafe)来调用。
二、Java中CAS是怎么实现的?
以AtomicInteger为例,看最核心的incrementAndGet()方法:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
再往下追getAndAddInt(简化版):此处的compareAndSwapInt方法会重新获取obj 在 offset 位置的值(即新v),与之前先获取的v值进行比较,相同时才会返回为true,否则为false。 注意:
- incrementAndGet并不会修改内存值,它只是调用了 getAndAddInt() 并在结果上加1,然后把最终的新值返回给调用者。
- 真正负责尝试修改内存(并且只有在 CAS 成功时才真正修改)的代码是 compareAndSwapInt
public final int getAndAddInt(Object obj, long offset, int delta) {
int v;
do {
v = getIntVolatile(obj, offset); // 读最新值
} while (!compareAndSwapInt(obj, offset, v, v + delta)); // CAS失败就重试
return v;
}
可以看到典型的自旋 + CAS 模式:
这种模式也被称为乐观锁(先干,冲突了再回滚重来)。
而synchronized和ReentrantLock属于悲观锁(先上锁再干)。
三、CAS的优缺点对比
| 是否阻塞 | 不阻塞(自旋) | 会阻塞 |
| 高竞争场景性能 | 可能退化严重(大量空自旋) | 相对稳定(线程进入阻塞队列) |
| 低竞争场景性能 | 极高(几乎无上下文切换) | 较重(至少有一次加锁/解锁开销) |
| 代码复杂度 | 相对复杂(需要自己处理重试逻辑) | 简单 |
| 是否可重入 | 本身无“锁”概念 | 支持重入 |
| 是否支持条件等待 | 不支持 | 支持(wait/notify、Condition) |
一句话总结适用场景:
- 读多写少、低冲突 → CAS 通常完胜
- 写多、冲突激烈 → synchronized / Lock 往往更稳定
四、最经典的问题:ABA问题
假设我们有一个栈顶指针(用AtomicReference实现),当前是:
A → B → C
↑
head
线程1想做 pop 操作:希望 head 从 A 变成 B
正常流程:
但真实世界可能发生下面这件事:
线程1 读到 head = A(准备CAS) ↓ 线程2 pop 了 A,head 变成 B 线程3 push 一个新的 A,head 又变回 A ↓ 线程1 执行 CAS(head, A, B) → 成功!
但这其实是错误的! 因为此时的 A 已经不是原来的 A 了(可能是新的对象,版本不同),会导致数据结构损坏。
ABA问题本质:只比较“值”是否相同,而没有比较“版本”是否相同。
业界主流解决方案
加版本号(最常见)
AtomicStampedReference / AtomicMarkableReference
// 带版本戳的引用
AtomicStampedReference<Node> head = new AtomicStampedReference<>(nodeA, 0);
// CAS时同时检查版本
head.compareAndSet(oldRef, newRef, oldStamp, oldStamp + 1);
只关心是否“动过”而不关心具体值 → 用AtomicMarkableReference(只用一个boolean标记)
五、常见的CAS使用
| 普通int/long自增 | AtomicInteger / AtomicLong | 日常最常用 |
| 高并发统计(Java8+) | LongAdder / DoubleAdder | 分段思想,性能远超Atomic* |
| boolean标志位 | AtomicBoolean | — |
| 引用类型 | AtomicReference | — |
| 带版本的引用 | AtomicStampedReference | 解决ABA |
| 只关心是否动过 | AtomicMarkableReference | 轻量级ABA方案 |
| 对象字段原子更新 | AtomicIntegerFieldUpdater 等 | 不改业务代码,适合遗留系统 |
| ConcurrentHashMap计数 | CounterCell + baseCount | 内部大量使用LongAdder思想 |
一个小建议:
当并发量不是特别高(< 几百qps),直接用LongAdder几乎总是比AtomicLong更优。
// 推荐写法(2024~2025主流)
private final LongAdder counter = new LongAdder();
public void incr() {
counter.increment();
}
public long get() {
return counter.sum();
}
六、总结一句话
CAS是Java并发包(JUC)最核心的原语之一,它让“无锁编程”成为可能,但也带来了自旋开销、ABA隐患等工程代价。
真正的高手不是“能不能用CAS”,而是“在什么场景下用哪种变体的CAS最合适”。
网硕互联帮助中心





评论前必须登录!
注册