atomicstampedreference 不自动修复 aba 问题,而是通过版本戳使 aba 可被检测;正确用法需原子读取值与戳、每次 cas 前重读并递增戳、循环中绝不复用旧戳;适用于地址复用场景,不适用于 id 唯一或对象不回收的场景。

ABA 问题不是“值变了又变回来”的 bug,而是 CAS 乐观判断失效导致的状态误判——它看到值没变,就以为中间没人动过,但其实对象已被回收、复用,甚至逻辑上下文已断裂。AtomicStampedReference 不是自动修复工具,而是把版本控制权交给你:它不消灭 ABA,只让 ABA 可被发现。
ABA 真正在哪出事?
典型高危场景集中在「物理地址复用」类操作:
- 无锁栈/队列中节点被 pop 后又 push 回去,引用相同但内存块已重用
- 对象池(如 ByteBuffer、Node)反复分配-释放-再分配,同一地址承载不同语义
- 缓存 key 被驱逐后又被新数据以相同 key 写回,旧 value 的状态残留误导后续更新
- 数据库行 ID 或业务单号被循环使用,CAS 基于 ID 判断时丢失变更历史
AtomicStampedReference 正确用法三步走
关键不在加了个 int,而在每一步都守住「值 + stamp」的原子一致性:
-
读必须用 get(int[]):声明
int[] stamp = new int[1],再调ref.get(stamp)。分开调getReference()和getStamp()会拿到跨快照的错配值 -
每次 compareAndSet 都要重读 + 递增 stamp:不能硬编码
compareAndSet(old, new, 0, 1),也不能缓存上一轮的stamp[0]。正确模式是:ref.get(stamp)→ 提取oldStamp = stamp[0]→ref.compareAndSet(old, next, oldStamp, oldStamp + 1) -
循环重试中 stamp 绝不复用:失败后直接下一轮,重新
get()拿最新 pair。若在成功后还拿旧stamp[0]做后续判断,那个 stamp 已失效
别踩这仨坑
很多人换了类却还是出问题,往往卡在这几个认知偏差上:
-
以为换类就自动解决:如果始终用
getReference()读值、硬写0当 stamp,那它和AtomicReference行为完全一样 - 把 stamp 当业务版本号:stamp 是计数器,不绑定语义。订单状态从 INIT→PROCESSING→DONE,更适合用固定值(0/1/2)而非自增,更清晰也防溢出
-
忽略初始 stamp 的意义:首次 CAS 的
expectedStamp就是整个版本链起点。设成0没问题,但若业务要求“必须从 1 开始”,那就得显式初始化为 1
它适合谁?不适合谁?
不是所有并发状态都需要它:
- 适合:节点复用频繁的无锁结构、资源池管理、底层缓存淘汰机制等“地址可能被重用”的场景
-
不适合:ID 全局唯一不重用、对象永不回收、开关型状态(开/关来回切)等 ABA 风险天然极低的场景。强行加 stamp 只会拖慢吞吐量(实测比
AtomicReference低 5%–15%)且增加维护成本
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











