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。使用时有三个要点:
- 用
get(int[] stampHolder)取得同一次快照中的引用和版本戳。分别调用getReference()和getStamp(),两次读取之间可能发生更新,组合出不属于同一状态的数据。 - 调用
compareAndSet(expectedReference, newReference, expectedStamp, newStamp)。四个参数依次是预期引用、新引用、预期版本戳和新版本戳。引用使用==比较,同时要求当前版本戳等于读取时保存的预期版本戳,成功后一起更新二者。 - 所有修改方都遵守版本推进协议。
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:依据包说明中单变量原子操作、版本戳与逻辑删除标记的区别,界定正文中原子类对可变对象内部状态的保护范围。


