AtomicStampedReference的compareAndSet需传四个参数,因其必须同时校验引用和版本号以防止ABA问题,漏掉任一参数将丧失版本控制能力。

AtomicStampedReference 的 compareAndSet 为什么必须传四个参数
因为 compareAndSet 不是“值 + 新值”两参数接口,它的语义是:只有当前引用和当前版本号都匹配预期值,才允许更新为新引用和新版本号。漏掉任意一个,就等于放弃对 ABA 的检测能力。
常见错误是沿用 AtomicReference.compareAndSet 的习惯,只传两个参数——这在编译期就会报错:method compareAndSet(V,V,int,int) is not applicable。JDK 强制你面对版本号的存在。
-
expectedReference和expectedStamp必须来自同一时刻的快照(推荐用get(int[])一次性读取) -
newStamp不能硬编码为expectedStamp + 1后就不管了——如果重试时expectedStamp已过期,+1 依然会失败 - 引用比较用的是
==,不是equals();如果你存的是可变对象,注意别被内容相等误导
初始化时 stamp=0 是安全的,但不能假设它永远不变
初始 stamp 设为 0 没问题,但后续所有 CAS 都必须基于最新读到的 stamp 值推进。很多人在循环里第一次读出 stamp = 0,后面就一直用 0 → 1 → 2 硬递增,结果在高竞争下因中间 stamp 已跳到 5,导致 CAS 永远失败。
- 正确做法:每次循环开头调用
ref.get(stampHolder)或分别调用getReference()和getStamp() -
int[] stampHolder = {0}是常用技巧,get(int[])会把当前 stamp 写入数组首元素,保证值与 stamp 原子对齐 - stamp 溢出风险极低(
Integer.MAX_VALUE级别),但若业务有超长生命周期且高频更新,需考虑 wrap-around 行为是否可接受
AtomicStampedReference 比 AtomicReference 多哪些开销
它底层仍是 Unsafe CAS,但每次操作要读写两个字段:引用 + int stamp,打包在 Pair 对象里。这意味着:
- 内存占用略高:每个实例多持有一个
int,且Pair对象本身有对象头开销 - 缓存行压力更大:若引用和 stamp 跨缓存行,可能触发 false sharing;实测吞吐量通常低
5%–15% - Android 兼容性要注意:API Level
24缺少getStamp()等方法,调用会抛NoSuchMethodError - 不适用于纯数值场景:如果是
int计数,优先用AtomicInteger;只有涉及对象引用变更且存在逻辑依赖中间状态时,才值得引入
ABA 问题不是总要解决,关键看业务是否感知“中间态”
很多场景下,只要最终值正确,中间是否被改过并不影响结果。比如计数器累加、开关状态翻转——这时用 AtomicReference 或 AtomicInteger 更轻量。
真正需要 AtomicStampedReference 的,是那些“值相同但含义已变”的情况:
- 无锁栈/队列中的节点复用(节点对象被弹出又压入,但内部状态已清空)
- 资源句柄或连接对象被关闭后又重置,但外部仍持有旧引用
- 配置项被临时覆盖又还原,但监听器需要知道“是否发生过变更”
最容易被忽略的一点:它不消除 ABA,只是让 ABA 可被检测。你得在 compareAndSet 失败后,决定是重试、告警,还是走降级逻辑——这个决策不在原子类里,而在你的业务代码中。

















