欢迎光临
我们一直在努力

CAS 的 ABA 问题:值没变,就真的没被改过吗?

在并发编程中,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 比较的不只是值,而是“值 + 版本”。

赞(0)
未经允许不得转载:171主机测评 » CAS 的 ABA 问题:值没变,就真的没被改过吗?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址