在并发编程中,CAS 是一个非常重要的概念。
很多 Java 并发工具类,比如 AtomicInteger、AtomicLong、AtomicReference,底层都离不开 CAS。它的思想很简单:更新数据之前,先判断当前值是不是自己期望的值。如果是,就更新;如果不是,就说明数据被别人改过,本次更新失败。
听起来很完美,但 CAS 有一个经典问题:ABA 问题。
这篇文章就聊聊 ABA 到底是什么,它为什么会出问题,以及 Java 中通常怎么解决。
一、CAS 是什么
CAS,全称是 Compare And Swap,比较并交换。
它做的事情可以理解成:
if (当前值 == 期望值) {
当前值 = 新值;
return true;
} else {
return false;
}
比如:
AtomicInteger num = new AtomicInteger(10);
boolean success = num.compareAndSet(10, 20);
这段代码的意思是:
如果 num 当前值是 10,就把它改成 20;如果当前值不是 10,说明它已经被其他线程改过了,那就更新失败。
注意,真正的 CAS 不是普通的 if 判断,而是 CPU 层面的原子操作。“比较”和“交换”这两个动作不会被其他线程打断。
这也是 CAS 能用于无锁并发编程的原因。
二、什么是 ABA 问题
ABA 问题指的是:
一个变量原来是 A,后来被改成了 B,然后又被改回了 A。
此时另一个线程来执行 CAS,发现当前值还是 A,于是认为这个变量没有被修改过,CAS 成功。
但实际上,这个变量中间已经被别人动过了。
举个简单例子。
假设共享变量初始值是 100。
线程 1 读取到值是 100,准备把它改成 200:
compareAndSet(100, 200)
但在线程 1 执行 CAS 之前,线程 2 先执行了两步操作:
100 -> 101
101 -> 100
这时候,变量的值又变回了 100。
线程 1 再执行 CAS 时,会发现当前值仍然是 100,于是 CAS 成功。
从 CAS 的角度看,它没有发现任何问题。因为它只判断“当前值是不是 100”。
但从并发语义上看,这个值已经被其他线程修改过,只是最后又改回来了。
这就是 ABA 问题。
三、ABA 为什么是问题
很多人第一次看到 ABA 问题时,会有一个疑问:
既然值最后又变回来了,那不就等于没变吗?
不一定。
这取决于业务是否关心“中间过程”。
如果我们只关心最终值,那么 ABA 可能确实没什么影响。
但如果我们关心的是“这个值在我操作期间有没有被别人改过”,那 ABA 就会带来问题。
CAS 最大的局限是:它只能看到当前结果,看不到中间过程。
它看到的是
A -> A
但真实发生的是:
A -> B -> A
四、一个典型场景:无锁栈
ABA 问题最经典的场景,是无锁栈。
假设现在有一个栈,栈顶是节点 A:
top -> A -> B -> C
线程 1 准备执行出栈操作。它先读到栈顶是 A,并且知道 A 的下一个节点是 B。于是它准备通过 CAS 把栈顶从 A 改成 B。
也就是:
CAS(top, A, B)
但在线程 1 真正执行 CAS 之前,线程 2 抢先执行了操作。
线程 2 先把 A 出栈:
top -> B -> C
然后又做了一些操作,最后把 A 重新放回栈顶:
top -> A -> C
这时候线程 1 再执行 CAS。
它发现栈顶还是 A,于是 CAS 成功,把 top 改成 B。
问题来了:此时 B 可能已经不在原来的位置了,甚至整个链表结构已经变化了。线程 1 还拿着之前观察到的旧关系继续操作,就可能导致节点丢失、链表断裂或者数据结构错乱。
所以 ABA 问题真正危险的地方在于:
值虽然回来了,但它背后的状态可能已经不是原来的状态了。
五、ABA 一定会导致问题吗
不一定。
这是一个很重要的点。
ABA 是 CAS 的潜在风险,但不是所有 CAS 场景都会被 ABA 影响。
比如简单计数器:
AtomicInteger count = new AtomicInteger(0);
如果业务只关心当前数值,那么中间是否从 0 变成 1,再变回 0,可能并不重要。
但在下面这些场景里,ABA 就需要特别小心:
无锁栈、无锁队列、链表指针更新;
资源状态流转,比如占用、释放、再次占用;
账户、库存、订单状态等对变化过程敏感的业务;
需要判断“数据在读取后是否被修改过”的并发更新场景。
所以,不能简单说“CAS 有 ABA,所以 CAS 不安全”。
更准确的说法是:
CAS 只能判断当前值是否等于期望值,不能判断这个值在此期间是否发生过变化。
六、如何解决 ABA 问题
解决 ABA 的核心思路是:
不要只比较值,还要比较版本号。
也就是说,原来我们比较的是:
值是不是 A
现在我们比较的是:
值是不是 A,并且版本号是不是原来的版本
只要中间发生过修改,即使值又变回 A,版本号也会发生变化。
比如一开始是:
(A, 1)
被修改成 B:
(B, 2)
又被改回 A:
(A, 3)
虽然值还是 A,但版本号已经从 1 变成了 3。
这时候,线程再拿着旧的 (A, 1) 去做 CAS,就会失败。
七、AtomicStampedReference
Java 中解决 ABA 问题的典型工具是 AtomicStampedReference。
它不仅保存引用值,还保存一个版本号,也叫 stamp。
示例代码:
AtomicStampedReference<String> ref =
new AtomicStampedReference<>("A", 1);
int stamp = ref.getStamp();
String value = ref.getReference();
boolean success = ref.compareAndSet(
value,
"B",
stamp,
stamp + 1
);
这里的 CAS 不再只是比较值,而是同时比较两样东西:
当前引用是不是期望引用;
当前版本号是不是期望版本号。
只有两个都匹配,更新才会成功。
这样就可以识别 ABA。
比如线程 1 看到的是:
(A, 1)
线程 2 把它改成:
(B, 2)
然后又改回:
(A, 3)
线程 1 再用 (A, 1) 去更新时,虽然值还是 A,但版本号已经不是 1,所以 CAS 会失败。
这就是 AtomicStampedReference 解决 ABA 的原理。
八、总结
CAS 是一种非常重要的无锁并发机制,它通过“比较并交换”来保证更新操作的原子性。
但 CAS 只比较当前值,不关心这个值中间是否被修改过。
ABA 问题的本质就是:
值从 A 变成 B,又变回 A,CAS 只能看到最终还是 A,却看不到中间发生过变化。
如果业务只关心最终值,ABA 可能不是问题;但如果业务关心状态是否被别人修改过,ABA 就可能带来严重的并发 Bug。
解决 ABA 的核心思路是增加版本号,让 CAS 比较的不只是值,而是“值 + 版本”。





