aba问题本质是值未变但中间被修改又恢复,cas仅比对最终值导致误判;版本号通过“值+版本号”双校验,以单调递增戳标记每次修改,使绕回原值时版本不匹配而失败。

CAS原子指令中的ABA问题,本质是“值没变,但中间被改过又改回来”,而CAS只比对最终值是否匹配,不关心过程变化,导致误判成功。版本号戳防线不是加一层保险,而是把“值是否相等”升级为“值+版本号必须同时匹配”,从逻辑上堵住这个漏洞。
ABA问题是怎么发生的
它依赖一个看似合理、实则危险的假设:值没变 = 状态没变。但多线程下,这个等式不成立。
- 线程A读取共享变量为A,准备用CAS改成C;
- 此时线程B介入:先将A改成B,再把B改回A;
- 线程A恢复后发现值仍是A,就执行CAS成功——但它完全不知道中间已发生两次修改。
典型危害场景包括无锁栈出栈错删节点、链表头节点被重复释放、账户余额翻转后触发重复扣款等。这些都不是“结果错了”,而是“逻辑断层了”:程序以为自己在操作原始对象,实际面对的是一个被重用、状态已不同的新对象。
为什么单纯加个版本号就能破局
因为版本号把“静态快照比对”变成了“带序号的状态校验”。只要变量被修改过一次,版本号就递增,哪怕值绕回原样,版本号也已不同。
- 初始:值=A,版本号=1;
- 线程B改A→B→A:值回到A,但版本号变成3;
- 线程A拿着“预期值=A + 预期版本号=1”去CAS,实际是“A+3”,直接失败。
这不是靠运气防错,而是用单调递增的整数给每一次修改打上不可伪造的时间戳(逻辑时间,非真实时间),让“被改过”这件事可检测、可拒绝。
Java里怎么正确用AtomicStampedReference
关键不在声明,而在读取和更新的配合方式。常见错误是分开调用getReference()和getStamp(),这中间可能已被其他线程更新。
- 必须用
get(int[] stampHolder)原子读取:它一次性返回当前值和版本号,确保两者配套; - CAS调用要传四个参数:
compareAndSet(expectedRef, newRef, expectedStamp, newStamp),缺一不可; - 构造时设好初始值和初始版本号,比如
new AtomicStampedReference("A", 1); - 注意int版版本号会溢出(到
Integer.MAX_VALUE后变Integer.MIN_VALUE),长期高并发系统需评估风险,必要时自定义长整型版本机制。
版本号不是万能解药,得看场景
它解决的是“需要感知修改轨迹”的问题。如果业务根本不依赖中间状态,加版本号反而增加开销。
- 适合加:链表节点删除、资源引用管理、状态机跃迁(如“待审核→审核中→待审核”需识别是否二次进入);
- 不必加:计数器累加(100→101→102,值变了就不构成ABA)、开关切换(true↔false,来回切也不影响语义);
- 慎用时间戳替代:System.nanoTime()不保证跨核单调,且精度高反而易因时钟漂移出错。











