ABA问题源于CAS仅比较值而忽略变更历史,导致无锁栈中栈顶节点被复用时产生逻辑错误;解决方法是用AtomicStampedReference引入版本号,使CAS同时校验引用和stamp,从而区分“未变”与“变回”。

Java 中 CAS 实现无锁栈时,ABA 问题会导致栈顶节点被“看似未变”地复用,从而引发逻辑错误(比如弹出一个已被回收又重新压入的节点,实际指向已失效内存)。单纯靠值比较无法区分“从未变过”和“变过又变回来”,所以需要引入版本号或时间戳来扩展 CAS 的判断维度——这就是 ABA 计数器(也称“带版本的 CAS”)的核心思想。
为什么无锁栈特别容易受 ABA 影响
无锁栈通常用 AtomicReference<node></node> 维护栈顶指针,每个 Node 包含 value 和 next。典型 push/pop 操作依赖 CAS 修改栈顶:
- push:期望旧顶点为
oldTop,新顶点为newNode,newNode.next = oldTop; - pop:期望旧顶点为
oldTop,新顶点为oldTop.next。
一旦某个线程在 pop 前被暂停,另一线程完成「pop → 修改节点 → push 回去」,原线程恢复后仍看到相同 oldTop 地址,CAS 成功但语义错误——它以为栈顶没动,实则中间节点已被重用。
ABA 计数器怎么防止恶意篡改
不是靠“防恶意”,而是消除歧义:把单个地址值扩展为 <node stamp></node> 二元组,每次修改都递增 stamp。CAS 不再只比 node 引用,而是同时比 node + stamp。
立即学习“Java免费学习笔记(深入)”;
Java 提供了现成工具:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
-
AtomicStampedReference<v></v>:封装V和int stamp,compareAndSet(V expectedRef, V newRef, int expectedStamp, int newStamp) -
AtomicMarkableReference<v></v>:用布尔标记代替计数(适合只需“是否被改过”的场景)
以无锁栈为例,可将栈顶定义为:
private AtomicStampedReference<Node> top = new AtomicStampedReference<>(null, 0);
push 操作需:
- 读取当前
top.getReference()和top.getStamp() - 构造新节点,
newNode.next = oldTop - 调用
top.compareAndSet(oldTop, newNode, oldStamp, oldStamp + 1)
pop 操作需:
- 读取
top.get()得(oldTop, oldStamp) - 若
oldTop != null,则期望新顶为oldTop.next,stamp 递增 -
top.compareAndSet(oldTop, oldTop.next, oldStamp, oldStamp + 1)
这样,哪怕 oldTop 地址被重复使用,其 stamp 已不同,CAS 必然失败,迫使线程重试并感知状态变化。
注意事项
- stamp 是有界整数(int),可能溢出。实际中极少成为瓶颈(每秒百万次操作也要 5000 年才溢出),但若业务要求极高可靠性,可用
Long自定义封装或借助AtomicReference<versionednode></versionednode>手动管理版本。 - stamp 本身不保证全局唯一,只保证同一引用被修改时 stamp 一定不同——这已足够打破 ABA 歧义。
- 不要混淆“防止恶意篡改”:ABA 计数器解决的是并发逻辑正确性问题,不是安全防护机制;它无法阻止 JVM 外部的内存篡改或反射破坏。
本质上,ABA 计数器把“值是否相等”的一维判断,升级为“值+演化阶段是否匹配”的二维判断。无锁栈的正确性,正依赖这种对历史变更的显式追踪。

















