CAS 的 ABA 问题如何解决?

考查重点:值变化历史与版本比较。给值配单调版本,CAS 同时比值和 stamp,A回原值时版本仍不同;确保版本更新和回收语义正确

本题目录

参考答案

核心回答

ABA 指线程先读到 A,准备更新前,其他线程已把共享值从 A 改为 B、再改回 A。普通 CAS 只比较当前值与预期值,因此仍可能成功;它没有承诺“从读取到现在一直没人改过”。如果业务还依赖期间的修改历史或旧的关联状态,就可能出错。

常见办法是把值和版本号作为一个整体原子更新:读取时得到 (A, 0),其他更新把它推进到 (B, 1)、(A, 2);旧线程仍拿 (A, 0) 比较,就会失败。Java 可以用 AtomicStampedReference 实现带整数版本戳的引用更新。若业务逻辑复杂,也可以用同一把锁保护完整的读取、判断和修改过程。

为什么只比较 A 不一定够

以可复用节点的无锁栈为例,线程 T1 读到栈顶节点 A 和旧后继 B,准备把栈顶从 A 改为 B。它暂停期间,T2 弹出 A、再弹出 B,最后把 A 重新压回栈顶。此时栈顶仍是同一个 A,但 A 的后继已经变了。T1 只比较栈顶引用,仍可能把旧的 B 写回,破坏栈的逻辑。

这里出问题的是 T1 使用了已经过时的结构关系。若某个算法只关心当前数值,中间变化不会破坏任何前提,A 变回 A 未必构成业务错误。因此,不能把任何一次 A→B→A 都直接判断为 CAS 失效。

Java 的版本戳应如何使用

AtomicStampedReference 保存引用与整数 stamp。使用时有三个要点:

  1. 用 get(int[] stampHolder) 取得同一次快照中的引用和版本戳。分别调用 getReference() 和 getStamp(),两次读取之间可能发生更新,组合出不属于同一状态的数据。
  2. 调用 compareAndSet(expectedReference, newReference, expectedStamp, newStamp)。四个参数依次是预期引用、新引用、预期版本戳和新版本戳。引用使用 == 比较,同时要求当前版本戳等于读取时保存的预期版本戳,成功后一起更新二者。
  3. 所有修改方都遵守版本推进协议。stamp 由调用方提供,类本身不会自动加一;也不能在提交前重新读取最新版本,替换原先的预期版本,那样就绕过了对旧状态的检查。

例如 T1 保存 (A, 0);T2 完成 (A, 0)→(B, 1)→(A, 2) 后,T1 用旧版本 0 尝试更新必须失败。失败后应重新读取完整状态,重新判断业务条件,不能只把版本改成 2 后沿用原先算出的旧结果。

方案有哪些边界

版本必须在相关观察窗口内不重复。 stamp 是有限的整数,可能回绕,也可能被错误地重置或复用。使用整数版本不能宣称在任意长时间、任意更新次数下都绝对没有 ABA;应结合状态生命周期和更新规模设计版本或采用其他同步方式。

原子引用不等于对象所有字段都安全。 换引用是原子的,但其他线程若绕过协议直接修改引用对象内部字段,版本戳并不会自动感知。可以把被比较的状态设计为不可变对象,每次构造新状态再原子替换,或者统一加锁保护相关字段。

布尔标记不是通用版本号。 AtomicMarkableReference 常用于表达节点是否被逻辑删除,只有两个标记值。若允许标记反复变化,它本身也可能回到原值,不能当作任意修改历史的计数器。

如果用锁,需要让所有相关访问遵守同一套同步规则,并在锁内重新读取和判断。只在最终写入时加锁,仍使用锁外取得的旧状态,不能解决旧状态失效的问题。

常见追问

加 volatile 能解决 ABA 吗?

不能。它处理相应的可见性和有序性,不记录修改历史,也不把“读取、计算、更新”自动合成一个原子业务操作。

Java 有 GC,是否就没有 ABA?

GC 有助于避免某些手工内存回收场景中的悬空引用,但应用仍可能把同一个对象取出后重新使用,或者让数值往返变化。逻辑上的 ABA 仍然需要算法处理。

CAS 失败是否直接重试就好?

应重新读取并重新计算,并考虑冲突时的重试成本;重试中的计算还要避免产生重复外部副作用。版本机制解决旧状态识别,不保证高竞争下始终低延迟。

参考资料

  • 国内:JavaGuide:CAS 详解:借鉴 ABA 与 CAS 局限的分析,区分正文中当前值相等与期间未被修改,并说明同时比较版本及竞争重试的代价。
  • 国内:JavaGuide:Atomic 原子类总结:采用引用类型原子类与版本戳示例,解释正文中一致读取引用和版本、四个更新参数及整数版本戳与布尔标记的区别。
  • 国内:VIBE:多线程 CAS 机制:依据读取旧值时保存版本、提交时比较旧版本的说明,解释正文中不能临时用最新版本替代预期版本的原因。
  • 国外:Java SE 25:AtomicStampedReference:类与方法契约明确了引用使用 == 比较、引用与版本戳一起更新,以及 get 获取二者的方式,用于核对 API 的实际含义。
  • 国外:Java SE 25:java.util.concurrent.atomic:依据包说明中单变量原子操作、版本戳与逻辑删除标记的区别,界定正文中原子类对可变对象内部状态的保护范围。

相关面经

5 篇

启云方

AI 应用开发工程师
一面
查看原面经 →
CAS 是什么?有什么问题?ABA 问题怎么解决?

中科星图维天信

后端开发
实习
查看原面经 →
请解释 CAS 的 ABA 问题,以及如何解决?

去哪儿

后端开发工程师
查看原面经 →
追问
你提到了CAS存在ABA问题,那在实际业务开发中,你会用什么方案解决这个问题?

小红书

AI Native 全栈开发工程师
日常实习一面、二面
查看原面经 →
CAS 原理、ABA、自旋问题

字节跳动

后端开发工程师
一面
查看原面经 →
了解 CAS 的 ABA 问题吗?
延伸问题
ABA 问题是如何产生的?
追问
ABA 问题有哪些解决方案?

相关题目

4 题