Appearance
CAS 的 ABA 问题怎么理解
CAS 很容易被理解成一句口号: “比较一下,没变就更新”。
真正麻烦的地方不是 CAS 本身,而是它只能看到“当前值”,看不到“值在这个过程中是否被别人改动过”。
ABA 问题正是这个盲点的典型体现。
先说结论
- CAS 适合做单变量的原子更新,不适合承载复杂业务状态协同
- ABA 的本质是“结果没变,但过程已经被别人动过了”
- 如果业务关心“过程是否发生过变化”,就不能只比较值,通常要引入版本号、时间戳或状态机
AtomicStampedReference能解决一类 ABA 问题,但不是所有并发问题都该用它
CAS 先别神化
CAS 的完整语义是:
- 读取当前值
- 拿旧值和内存中的值比较
- 如果相同,就更新成新值
- 如果不同,就重试
伪代码可以写成:
java
while (true) {
int current = value.get();
int next = current + 1;
if (value.compareAndSet(current, next)) {
break;
}
}它的优点很明确:
- 不需要阻塞挂起线程
- 低竞争场景下吞吐不错
- 很适合计数器、状态位、游标推进这类简单原子更新
但它的前提同样明确:
- 只能对一个变量的当前值做判断
- 无法天然表达“中间是否发生过变化”
ABA 问题到底是什么
假设线程 T1 读取到变量值为 A,准备把它改成 C。
这时线程 T2 先把 A 改成 B,又把 B 改回 A。
等 T1 再来做 CAS 时,看到值还是 A,于是 CAS 成功。
从 T1 视角看,值“没变”;
但从系统真实过程看,这个值已经经历过一次变化。
这就是 ABA:
- A -> B -> A
- 结果看似一样
- 过程已经不同
为什么有些场景不怕 ABA
不是所有 CAS 都要解决 ABA。
例如一个纯计数器,只关心当前值能否从 n 更新到 n + 1,并不关心值中间有没有被别人改过又改回来。
这种场景,ABA 并不会带来语义错误。
所以判断重点不是“有没有 ABA”,而是:
- 业务是否关心这段期间对象是否被动过
- 当前值是否只是数值,还是代表某个资源身份或对象引用
真正危险的场景
1. 无锁栈、无锁链表
节点引用可能被弹出后又重新挂回去,CAS 只看到“头指针还是同一个地址”,却不知道中间结构已经变了。
2. 状态机推进
比如订单状态从“待支付”到“已取消”再补偿回“待支付”,
此时你不能因为值又回到“待支付”就认为过程没发生过。
3. 资源复用
对象池、连接池、缓存槽位这些场景里,“同一个引用值”不一定代表“同一个语义状态”。
怎么解决 ABA
1. 加版本号
最常见的方法是把“值”和“版本”一起比较。
只要版本号变了,即使值一样,也认为状态发生过变化。
这正是 AtomicStampedReference 的思路。
java
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 1);更新时需要同时比较:
- 当前引用值
- 当前 stamp
只要 stamp 不一致,CAS 就失败。
2. 加标记位
如果不需要递增版本,只关心“是否被改动过”,也可以用 AtomicMarkableReference。
3. 上升一个抽象层
很多业务系统里,最稳的做法并不是引入更复杂的 Atomic 类,而是:
- 数据库行版本号
- 乐观锁字段
- 明确的状态机
- 幂等表或去重表
也就是说,把并发语义交给更适合承载业务一致性的模型。
工程上怎么选
场景一: 计数器、自增序号、热点统计
优先考虑 AtomicLong、LongAdder。
这类场景一般不需要关心 ABA。
场景二: 状态位切换
如果只是 INIT -> RUNNING -> STOPPED 这种单变量状态切换,可以用 CAS,
但要确保状态转换图是闭合且受控的。
场景三: 引用对象、链表结构、可回收节点
要重点防 ABA,必要时上版本号或换数据结构。
常见误区
1. 以为 CAS 一定比加锁高级
CAS 只是另一种同步手段,不是性能万能药。
高竞争下反复自旋同样会浪费 CPU。
2. 用 Atomic 类硬做多字段一致性
CAS 最适合单变量。
一旦你想同时维护多个字段的一致性,往往就已经超出它的舒适区了。
3. 只知道 AtomicStampedReference,不知道为什么要用
很多人背得出类名,却没想清楚业务是不是“真的关心过程被改动过”。
没有这个判断,方案很容易过度设计。
一个实用判断
当你准备上 CAS 时,先问自己:
- 我更新的是一个数值,还是一个有身份语义的对象/状态
- 我关心最终值,还是关心中间是否被改动过
- 失败重试的成本高不高,竞争激烈时会不会空转
如果你关心的是“过程发生过变化”,那就不要只比较值。
总结
ABA 问题不是 CAS 的冷门细节,而是它能力边界的直接体现。
CAS 能看见“现在是什么”,但看不见“刚才经历过什么”。
所以真正的重点不是会不会背 AtomicStampedReference,而是知道什么时候只比较值已经不够用了。