可以把它拆成两部分理解:CAS 是怎么做到“无锁更新”的,以及 ABA 为什么会让 CAS 误判成功。
一、CAS 的原理
CAS 全称是 Compare And Swap,也叫 Compare And Set。
它的核心逻辑很简单:
拿内存中的当前值 V,和预期值 A 比较;如果相等,就把它改成新值 B;如果不相等,就什么都不做。
可以理解成下面这个伪代码:
if (value == expected) {
value = newValue;
return true;
} else {
return false;
}
但重点在于:
1)这是一个原子操作
这个比较 + 更新,不是两步分开执行,而是 CPU 指令级别保证原子性。
也就是说,中间不会被别的线程插进来修改。
Java 里常见的 CAS 类,比如:
AtomicIntegerAtomicLongAtomicReference
底层最终都会依赖 JVM 和 CPU 提供的原子指令来完成。
2)CAS 的工作流程
以 AtomicInteger.incrementAndGet() 为例,本质上通常是这样的“自旋”逻辑:
for (;;) {
int oldValue = get();
int newValue = oldValue + 1;
if (compareAndSet(oldValue, newValue)) {
return newValue;
}
}
含义是:
- 先读取当前值
- 基于旧值计算新值
- 尝试 CAS 更新
- 如果失败,说明有别的线程先改了,重新再来一次
这种不断重试的方式,就叫 自旋。
3)CAS 为什么高效
和 synchronized 相比,CAS 的优势是:
- 不会阻塞线程
- 不会发生线程挂起和唤醒的上下文切换
- 在低竞争场景下性能很好
所以它常被称为一种 乐观锁 思想:
我先假设没有人跟我冲突,更新失败了再重试。
二、CAS 的问题
CAS 虽然高效,但不是万能的,常见有三个问题:
1)自旋开销大
如果竞争很激烈,一个线程可能一直 CAS 失败,不断重试,浪费 CPU。
2)只能保证一个共享变量的原子操作
如果要同时更新多个变量,单纯 CAS 不够,需要:
- 加锁
- 或把多个字段封装成一个对象,用
AtomicReference - 或使用
AtomicStampedReference/AtomicMarkableReference
3)ABA 问题
这是最经典的问题。
三、什么是 ABA 问题
现象
CAS 只会判断:
“当前值是否还等于我最初看到的值?”
它不会关心这个值中间有没有变过。
举个例子:
- 线程1读取到变量值是
A - 在线程1准备 CAS 之前
- 线程2把值从
A改成了B - 然后又从
B改回了A - 线程1再去 CAS,发现值还是
A - 于是 CAS 成功
线程1会以为:
“这个值一直没变过”
但实际上它已经被别人改过两次了。
这就是 ABA 问题。
四、ABA 为什么有问题
如果只是普通整数计数,有时 ABA 未必造成严重影响。
但在一些场景下会出大问题,比如:
- 无锁栈
- 无锁队列
- 链表节点更新
- 对象引用更新
因为这里关心的不只是“值相等”,还关心:
这个对象 / 节点是否还是原来那个状态
例如栈顶节点原来是 A,后来被弹出又重新压回,虽然“看起来还是 A”,但它的 next 指针、上下文状态可能已经变了。
这时 CAS 误判成功,就可能导致数据错乱。
五、ABA 怎么解决
方案1:加版本号 / 时间戳
最常见、最标准的方案就是:
比较时不仅比较值,还比较版本号。
每次修改时:
- 值更新
- 版本号也 +1
这样即使值从 A → B → A,只要版本号变了,就能识别出“它被改过”。
Java 里的实现
Java 提供了:
AtomicStampedReference
它内部维护:
- 引用值
reference - 版本号
stamp
示意:
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 1);
更新时不仅要传值,还要传版本号:
int stamp = ref.getStamp();
String value = ref.getReference();
boolean success = ref.compareAndSet("A", "B", stamp, stamp + 1);
如果中间有人改过,即使值又变回 "A",版本号也对不上,CAS 就会失败。
方案2:使用 AtomicMarkableReference
如果你不需要精确版本号,只需要知道:
“这个对象是否被动过 / 是否被标记过”
可以用:
AtomicMarkableReference
它维护的是:
- 引用值
- 一个 boolean 标记位
适合某些“逻辑删除”场景,但它没有版本号那么强。
方案3:直接加锁
如果场景复杂,且对一致性要求很高,直接用:
synchronizedReentrantLock
有时候反而更稳妥。
因为不是所有问题都适合靠 CAS 无锁解决。
六、一个 ABA 的经典例子
假设共享变量初始值为 100。
线程1:
读取到值为 100,准备改成 200
线程2:
先把 100 改成 101,再改回 100
这时线程1执行:
compareAndSet(100, 200)
会成功,因为当前值确实还是 100。
但线程1不知道这个值中间被别人改过。
七、面试回答
CAS 是一种基于硬件原子指令实现的无锁并发机制。它会比较内存位置当前值是否等于预期值,如果相等就更新为新值,否则更新失败。Java 中 AtomicInteger 等原子类底层就大量使用了 CAS,并结合自旋重试来实现线程安全。
CAS 的优点是避免了线程阻塞和上下文切换,在低竞争场景性能较好;缺点是高竞争下自旋开销大、只能保证单变量原子性,而且存在 ABA 问题。
ABA 的意思是一个值原来是 A,中间被改成 B,后来又改回 A,CAS 只判断当前值是否等于 A,因此会误以为它没有被改过。这个问题在链表、栈等基于引用的无锁结构里比较严重。
解决 ABA 的常见方式是给变量增加版本号或时间戳,Java 中可以使用 AtomicStampedReference,在比较值的同时比较版本号,这样即使值回到了原值,也能知道它中间发生过变化。
八、总结
CAS 原理
比较当前值和预期值,相等才原子更新,不相等就失败重试。
ABA 问题
值虽然回到了原值,但中间其实被改过,CAS 看不出来。
解决办法
给值加版本号,常用 AtomicStampedReference。