AtomicStampedReference 通过引用+版本戳双校验解决 ABA 问题,其 compareAndSet 需传四个参数以原子验证状态快照;stamp 由业务定义并手动维护,不可复用旧值,否则退化为 AtomicReference。

AtomicStampedReference 不是“给 AtomicReference 加个 int 字段”那么简单,它的版本戳设计直指 CAS 原子性失效的根源——ABA 问题本身无法靠单次值比对发现。
为什么 compareAndSet("A", "B") 在 ABA 场景下会误成功
当线程读到 reference == "A",就认为“值没被改过”,接着调用 compareAndSet("A", "B")。但中间可能已发生 "A" → "B" → "A",内存值虽回归原状,语义却已不同。AtomicReference 的 CAS 只校验引用地址是否一致(==),不感知历史变更,因此判定为“可更新”。这种误判在资源复用、状态机、保险柜开关等场景中会导致严重逻辑错误。
常见错误现象:
- 多线程反复修改同一对象引用,最终结果看似正确,但某次操作实际作用在已被“回收再分配”的旧实例上
- 状态流转本应只触发一次的动作(如发通知、扣库存),因 ABA 被重复执行
AtomicStampedReference 的 compareAndSet 为何必须传四个参数
compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) 这个签名不是为了增加复杂度,而是强制调用者显式携带“预期状态快照”。它底层将 expectedReference 和 expectedStamp 封装进一个不可变的 Pair 对象,再通过 Unsafe.compareAndSwapObject 原子替换整个 Pair。
关键点:
-
Pair是 final 的,避免被中途篡改;pair字段是volatile,保证可见性 - CAS 成功的前提是:当前
pair.reference == expectedReference且pair.stamp == expectedStamp—— 缺一不可 - 每次调用
set(V newRef, int newStamp)或attemptStamp都会生成新Pair,旧Pair实例即失效
stamp 参数不是“自增 ID”,而是业务语义的版本标识
很多人误以为 stamp 必须递增,其实它只是个整型标记,只要能反映状态变化即可。你可以用时间戳低几位、操作序号、甚至布尔翻转(0/1)——只要和引用变更保持逻辑绑定。
使用场景差异:
- 若业务中“同一值重复出现”有明确阶段划分(如订单状态:INIT→PROCESSING→INIT),可用枚举序号作
stamp - 若仅需区分“是否被中间修改过”,用
0/1翻转足够,此时AtomicMarkableReference更轻量 - 盲目用
System.nanoTime()当stamp可能引发 long 溢出或时钟回拨问题,不推荐
性能影响很小:多一次 int 比较,无额外锁或内存屏障;但要注意 get(int[] stampHolder) 中传入的数组长度必须 ≥1,否则抛 NullPointerException。
真正容易被忽略的是 stamp 的维护责任在调用方
AtomicStampedReference 不自动管理 stamp,它只做校验。你必须在每次读取后,把当前 stamp 一起记下来,下次 compareAndSet 时原样传回。漏掉这一步,或者复用旧 stamp,就等于退化回 AtomicReference。
典型错误写法:
int stamp = atomic.getStamp(); // 错!未与 reference 同步获取 atomic.compareAndSet(oldRef, newRef, stamp, stamp + 1); // stamp 可能已过期
正确做法是用 get(int[] holder) 一次性拿到最新 pair:
int[] stampHolder = new int[1]; V currentRef = atomic.get(stampHolder); int currentStamp = stampHolder[0]; // … 处理逻辑 … atomic.compareAndSet(currentRef, newRef, currentStamp, currentStamp + 1);
这个细节没有封装进 API,是因为设计者认为:版本语义必须由业务自己定义和维护,库不该越俎代庖。

















