
AtomicStampedReference 是什么,为什么它能解决 ABA
AtomicStampedReference 不是给变量“加锁”,而是给每次修改打上一个可验证的“戳”(stamp)。当多个线程反复把值从 A → B → A 时,普通 AtomicReference 会误判为“没变过”,而 AtomicStampedReference 同时检查值和 stamp:哪怕值回到 A,只要 stamp 已更新,就拒绝 CAS 成功。
这本质是用版本号打破“值相等即安全”的错觉。它不消除 ABA,而是让 ABA 可被检测——这才是关键。
常见错误现象:
- 用
compareAndSet(oldRef, newRef)但没传 stamp,编译直接报错(方法签名强制要求) - 把 stamp 当成时间戳用,手动递增却忘了多线程并发下可能重复(比如两个线程同时读到 stamp=1,都+1 写回 2)
- 初始化时用
new AtomicStampedReference(obj, 0),后续却在别处硬编码compareAndSet(obj, obj, 0, 1),导致第一次 CAS 就失败
怎么正确构造和使用 compareAndSet 核心是:**每次 CAS 都要基于当前最新 stamp 值来计算下一个 stamp**,不能靠猜测或固定偏移。
实操建议:
- 调用
get()拿到的是int[]数组(长度为 1),必须用getStamp()和getReference()分开取值,别直接解包 - 写 CAS 时,先用
get()读当前值和 stamp,再决定新值和新 stamp;不要“先算新 stamp,再读旧值”,顺序反了会出竞态 - stamp 类型是
int,溢出后会从Integer.MAX_VALUE回到Integer.MIN_VALUE,但绝大多数场景下,几百万次操作才可能撞上,不用 preemptively 处理
int[] stamp = new int[1]; String current = ref.get(stamp); // 一次读取值和 stamp int oldStamp = stamp[0]; String next = computeNextValue(current); // 正确:基于刚读到的 oldStamp 推进 boolean success = ref.compareAndSet(current, next, oldStamp, oldStamp + 1);
和 AtomicReference 的性能与兼容性差异
AtomicStampedReference 底层仍用 Unsafe CAS,但每次操作要读写两个字段(引用 + int 数组),比 AtomicReference 多一次内存访问。实测吞吐量低 5%–15%,取决于 CPU 缓存行对齐情况。
使用场景限制:
- 不适用于需要序列化传递 stamp 的场景——
int[]是内部状态,外部无法可靠重建 - 不能替代锁做复合逻辑(比如“先查再删再加”),它只保单次 CAS 安全;复杂流程还得靠
synchronized或StampedLock - Android API 24+ 才完整支持;低于此版本可能抛
NoSuchMethodError(因getStamp()等方法是后来加的)
容易被忽略的初始化和调试细节
最常被跳过的一步:**stamp 初始值不是“无所谓”,它参与所有 CAS 判断**。如果你在初始化时用了 new AtomicStampedReference(null, 100),那第一次 compareAndSet(null, x, 0, 1) 必定失败——因为期望的旧 stamp 是 0,实际是 100。
调试建议:
立即学习“Java免费学习笔记(深入)”;
- 打印日志时别只打值,一定要连同
getStamp()一起输出,例如:log("ref=" + ref.get() + ", stamp=" + ref.getStamp()) - 单元测试里故意构造 ABA:启两个线程,T1 读 A→改 B→休眠→改 A;T2 在 T1 改 B 后立刻执行 CAS,看是否被拒绝
- 别用
AtomicStampedReference<string></string>做字符串拼接类逻辑——字符串不可变,每次 new 都是新对象,stamp 很难复用,反而增加混乱
AtomicStampedReference 的代价很小,但前提是 stamp 的管理逻辑得跟值一样严谨——它不是开关,是个需要同步维护的状态。










