一、一句话理解
AtomicStampedReference是一个原子引用,它不仅维护一个对象引用,还附带一个整型“戳记(stamp)”,通过同时比较引用和戳记来实现更安全的 CAS(Compare-And-Swap)操作,从而避免经典的 ABA 问题。
二、什么是 ABA 问题?
场景举例:
假设有一个栈,初始状态为A → B → C,栈顶是 A。
- 线程1 读取栈顶为 A,准备执行 CAS(期望 A,替换为 D)。
- 此时线程2 弹出 A,再压入新节点 A’(内容相同但地址不同,或甚至同一个 A 被回收后重用)。
- 栈变成:
A → B → C(看起来没变)。 - 线程1 执行 CAS:发现栈顶仍是 A,于是成功替换为 D。
但实际上,中间发生了变化(A 被弹出又压入),但 CAS 无法感知,这就是ABA 问题。
💡 ABA 的本质:值看起来没变,但语义已变。
三、AtomicStampedReference如何解决 ABA?
它给每个引用附加一个版本号(stamp):
- 每次修改引用时,同时更新 stamp(比如 +1)。
- CAS 操作必须同时匹配引用 + stamp。
- 即使引用值“看起来一样”,只要 stamp 不同,就认为是不同状态。
这样就能区分:
- “真的是同一个状态” vs
- “值碰巧相同,但中间被篡改过”
四、核心设计解析
1.内部类Pair<T>
privatestaticclassPair<T>{finalTreference;finalintstamp;// ...}- 将引用 + stamp打包成一个不可变对象(
final字段)。 - 所有状态变更都通过原子地替换整个
Pair对象实现。
✅ 这是一种典型的“不可变快照”设计。
2.volatile 引用
privatevolatilePair<V>pair;- 保证多线程对
pair的读写具有可见性和happens-before语义。 - 但仅靠
volatile无法保证原子性(比如 compare-and-set),所以需要 CAS。
3.关键方法:compareAndSet
publicbooleancompareAndSet(VexpectedReference,VnewReference,intexpectedStamp,intnewStamp){Pair<V>current=pair;returnexpectedReference==current.reference&&// 引用相等(注意:是 ==,不是 equals!)expectedStamp==current.stamp&&((newReference==current.reference&&newStamp==current.stamp)||casPair(current,Pair.of(newReference,newStamp)));}注意点:
- 引用比较用
==:这是为了性能和语义一致性(通常用于指针/对象身份比较)。 - 如果新值和当前值完全一样,直接返回
true(避免不必要的 CAS)。 - 否则,调用底层
Unsafe.compareAndSwapObject原子替换pair。
🔒 底层依赖
sun.misc.Unsafe的 CAS 指令(硬件级原子操作)。
4.辅助方法
| 方法 | 作用 |
|---|---|
getReference()/getStamp() | 分别获取当前引用和戳记 |
get(int[] stampHolder) | 一次性获取引用和戳记(避免两次读之间状态变化) |
set(V, int) | 无条件设置新值(非原子组合操作,慎用) |
attemptStamp(V, int) | 只更新 stamp,不改变引用(前提是引用仍等于期望值) |
📌
attemptStamp常用于“标记”场景,比如标记某个节点已被逻辑删除。
5.weakCompareAndSet的说明
publicbooleanweakCompareAndSet(...){returncompareAndSet(...);// 目前 JDK 中两者实现相同}- 注释提到它“可能虚假失败,且不提供内存顺序保证”。
- 但在当前 HotSpot 实现中,它和
compareAndSet行为一致。 - 主要是为了未来在某些平台(如 ARM)上提供更弱但更快的 CAS 变体。
五、典型使用场景
1.无锁数据结构(Lock-Free Data Structures)
- 如无锁栈、队列、链表等,需要检测 ABA 问题。
- 示例:在实现
ConcurrentLinkedQueue或自定义无锁栈时,用AtomicStampedReference包装头指针。
2.状态机带版本控制
- 某个对象的状态变更需要记录“版本”,防止旧状态覆盖新状态。
3.资源回收标记
- 先用
attemptStamp将 stamp 设为“已删除”(如 -1),再安全回收。
六、使用示例
AtomicStampedReference<String>ref=newAtomicStampedReference<>("A",0);int[]holder=newint[1];Stringcurrent=ref.get(holder);intcurrentStamp=holder[0];// 尝试将 "A" (stamp=0) 替换为 "B" (stamp=1)booleansuccess=ref.compareAndSet("A","B",0,1);if(success){System.out.println("Update succeeded");}⚠️ 注意:
expectedReference必须是同一个对象引用(==成立),不能只是equals相等。
七、与AtomicReference对比
| 特性 | AtomicReference<V> | AtomicStampedReference<V> |
|---|---|---|
| 存储内容 | 仅引用 | 引用 + int stamp |
| 解决 ABA | ❌ 否 | ✅ 是 |
| 内存开销 | 小 | 稍大(多一个 int + Pair 对象) |
| 使用复杂度 | 简单 | 稍高(需管理 stamp) |
| 性能 | 更快 | 略慢(多一次 int 比较) |
八、注意事项
引用比较是
==,不是equals
→ 适用于对象身份比较,不适合值语义(除非你确保同一对象复用)。stamp 需要手动管理
→ 通常每次修改时stamp + 1,或用时间戳、逻辑版本号。不要滥用
→ 如果你的场景不可能出现 ABA(比如引用永远不会被回收重用),用AtomicReference更高效。Java 9+ 替代方案?
→ 虽然有VarHandle,但AtomicStampedReference仍是解决 ABA 的标准工具。
九、总结
AtomicStampedReference是 Java 并发编程中解决 ABA 问题的经典工具。它通过“引用 + 版本戳”的组合,使得 CAS 操作能够感知到中间状态的变化,从而保证无锁算法的正确性。
它不是日常开发常用类,但在实现高性能、无锁并发数据结构时,它是不可或缺的“利器”。
如果你正在编写 lock-free 或 wait-free 算法,或者遇到因对象复用导致的并发 bug,AtomicStampedReference很可能就是你需要的答案。