aba问题真实发生于多线程对同一atomicinteger反复执行“读取→计算→cas更新”且中间值被改回原值的场景,如线程a读100后挂起,线程b将100→200→100,a恢复后cas误判无人修改;cas仅比对数值不追踪状态变迁,故无法识别值同而中间状态已变。

ABA问题到底在什么场景下会真实发生
当多个线程对同一个 AtomicInteger 反复做「读取→计算→CAS更新」,且中间有其他线程把值改回去时,就可能出错。典型例子是:线程A读到值为100,被挂起;线程B把100→200→100(比如删除又重建了某个资源);线程A恢复后,用CAS判断“还是100”,就误认为没人动过它,直接提交变更——但其实中间状态已丢失。
CAS为什么无法感知“值相同但状态不同”
compareAndSet 只比对当前值是否等于预期值,不记录修改次数或时间戳。它本质是“乐观锁的简化版”,没内置版本号机制。所以:
- 不是线程安全漏洞,而是设计取舍:轻量、快,但放弃状态变迁追踪
- 所有基于
Unsafe.compareAndSwapInt的原子类(AtomicInteger、AtomicReference)都存在这个问题 - 在链表栈/队列的无锁实现中尤其危险,比如
pop时头节点被替换成相同地址的新节点,CAS会误判成功
用 AtomicStampedReference 带版本号修复
它把「值 + 整数戳」打包成一个不可变对,每次更新都要求值和戳同时匹配。关键点:
- 初始化必须传入初始值和初始戳(通常为0):
new AtomicStampedReference<string>("a", 0)</string> - 更新时要显式提供旧戳和新戳:
compareAndSet(oldVal, newVal, oldStamp, newStamp) - 获取当前戳要用
getStamp(),不能只靠get();读值也得用get(int[] stampHolder)才能一并拿到戳 - 戳溢出(
Integer.MAX_VALUE)会导致回绕,但实践中极少成为瓶颈;如需更强保证,可用AtomicMarkableReference(布尔标记)或自定义长整型版本
别以为加了戳就万事大吉
很多人以为只要换用 AtomicStampedReference 就自动解决ABA,其实容易踩三个坑:
立即学习“Java免费学习笔记(深入)”;
- 忘记同步更新戳:比如两次CAS之间没递增戳,等于白加
- 在循环重试逻辑里,没重新读取最新戳,导致用旧戳参与下一轮
compareAndSet - 误以为戳能代表“业务版本”——它只是计数器,不绑定业务语义;如果业务上100→200→100是合法状态迁移,那ABA根本不是问题,强行加戳反而增加复杂度
真正要盯住的,是那个被反复回收再分配的对象引用——比如池化节点、数据库行ID、缓存key。这些地方一旦复用,才是ABA风险高发区。









