volatile 保证变量可见性与禁止重排序,使 cas 能读到主内存最新值;但无法识别 aba 中“值相同但历史不同”的问题,需用 atomicstampedreference 等带版本号的原子类解决。

单纯用 volatile 无法解决 ABA 问题,它和 CAS 是配合使用的基础条件,但不是解决方案本身。
volatile 在 CAS 中起什么作用?
volatile 保证变量的可见性与禁止重排序,让每个线程读取到的是主内存中最新的值,而不是缓存副本。这是 CAS 能正确比较的前提——如果线程读到的“预期值”是过期的,CAS 就可能基于错误状态做判断。
例如:AtomicInteger 内部的 value 字段被声明为 volatile,这样 get() 总能拿到最新值,compareAndSet() 才有可靠依据。
为什么 volatile + CAS 还是会出 ABA?
因为 ABA 的本质不是“读不到新值”,而是“值变回去后看起来没变”。比如:
- 线程 A 读到值为 100(记作预期值 A)
- 线程 B 把 100 → 200 → 100(完成一次 A→B→A)
- 线程 A 执行
compareAndSet(100, 300),发现当前值仍是 100,于是成功更新为 300
整个过程里,volatile 确保了每次读都是实时的,但无法标记“这个 100 是不是原来的那个 100”。CAS 只比数值,不比历史。
真正解决 ABA 的方式:加版本戳
Java 提供了带版本号的原子引用类,把“值”和“修改次数”打包成一个不可分割的单元:
-
AtomicStampedReference<e></e>:内部维护[reference, stamp]二元组 -
AtomicMarkableReference<e></e>:用一个布尔标记表示是否被修改过
调用 compareAndSet(E expectedRef, E newRef, int expectedStamp, int newStamp) 时,必须同时满足值相等 且 版本号匹配,才能更新。哪怕值变回去了,只要 stamp 不同,CAS 就失败。
实际使用建议
多数业务场景下不需要主动处理 ABA,比如计数器、状态标志位等,值本身无业务含义,只关心最终结果。
只有在值具有“身份语义”时才需警惕 ABA,典型如:
- 基于链表实现的无锁栈/队列(节点被弹出又压入,指针可能复用)
- 资源池中对象的回收与复用(同一内存地址被反复分配释放)
这时应优先选用 AtomicStampedReference,并在设计时明确 stamp 的增长逻辑(如每修改一次 +1,或用时间戳)。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











