Java 里的 ABA 问题,核心不是“引用最后是不是又变回 A”,而是“这次 A 还是不是之前那次状态”。如果只用 AtomicReference 比较对象引用,A→B→A 可能让线程误以为期间没有发生变化。AtomicStampedReference 把引用和一个 int 类型的 stamp 原子绑定,CAS 时同时检查两项,正适合记录这种状态回转。
官方地址:https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/atomic/AtomicStampedReference.html
下面只讨论一个边界:如何读取引用与 stamp 的配对快照,以及如何用 compareAndSet 检测 ABA。它不是事务管理器,也不会自动理解对象内部字段是否发生变化。
先把 ABA 拆成引用身份和 stamp
假设共享位置最初保存对象 A,stamp 是 10。线程甲读取到“引用 A + stamp 10”后暂时停顿;线程乙先把它改成 B 并把 stamp 改成 11,再把引用改回 A,同时把 stamp 改成 12。此时从引用字段单独看,当前值确实又是 A,但完整状态已经从 (A, 10) 变成了 (A, 12)。
Java SE 25 API 对 AtomicStampedReference 的定义就是原子维护对象引用和整数 stamp。这里的 stamp 不是对象内容摘要,而是由更新方维护的状态标记;只要每次相关状态变化都分配不同的标记,读取方就能发现“引用回来了,但版本不一样”。
用 get 读取一份配对快照
不要先调用 getReference(),再隔一段逻辑调用 getStamp(),然后把两次读取当作同一时刻的快照。更直接的做法是调用 get(int[] stampHolder):返回值是当前引用,stamp 会写入传入数组的第一个元素。数组至少要有一个位置。
import java.util.concurrent.atomic.AtomicStampedReference;
public class AbaSnapshot {
public static void main(String[] args) {
Object first = new Object();
// 初始状态把引用和版本号绑定在同一个原子容器里。
AtomicStampedReference
这里的 observed 与 observedStamp 应一起保存,作为后续 CAS 的 expected 值。若代码只保存 observed 而丢掉 stamp,后续又会退化成只比较引用。
用 compareAndSet 同时检测两项
compareAndSet 的四个参数分别是期望引用、新引用、期望 stamp 和新 stamp。官方 API 明确要求:当前引用要与期望引用使用 == 相等,同时当前 stamp 也要等于期望 stamp,两个条件都满足才会原子更新。
import java.util.concurrent.atomic.AtomicStampedReference;
public class AbaCasCheck {
public static void main(String[] args) {
Object first = new Object();
Object second = new Object();
AtomicStampedReference
这个例子用 set 模拟其他线程的变化,重点是 CAS 的判定条件,不是把它当作并发测试结果。线程甲保存的是 (first, 10),当前配对状态是 (first, 12),因此即使引用身份仍然是同一个 first,stamp 不相等也足以拒绝更新。
每次相关状态变化都要更新 stamp
AtomicStampedReference 不会替调用方自动递增 stamp。使用 set、compareAndSet 或其他更新入口时,业务代码必须决定新 stamp。一个常见约定是成功更新后使用旧值加一,并让所有会改变“可被 CAS 观察的状态”的路径遵守同一约定。
// 读取引用与版本后,基于同一份快照尝试提交新配对状态。
int[] stampHolder = new int[1];
Object expectedReference = state.get(stampHolder);
int expectedStamp = stampHolder[0];
Object nextReference = new Object();
// 失败表示引用或 stamp 已被其他线程改变,需要重新读取再决定。
boolean success = state.compareAndSet(
expectedReference,
nextReference,
expectedStamp,
expectedStamp + 1);
失败后不要直接复用旧的 expectedReference 和 expectedStamp 无限重试。应该重新取得配对快照,并重新计算业务是否仍然允许更新;否则虽然避免了 ABA,却可能把已经过时的业务意图反复提交。
这套检测能解决什么,不能解决什么
| 场景 | AtomicStampedReference 的作用 | 调用方仍需负责 |
|---|---|---|
| 引用 A→B→A | stamp 变化后,旧的引用与旧 stamp 配对不再匹配 | 每次相关更新都维护新 stamp |
| 只修改对象 A 的内部字段 | 不会自动观察字段内容变化 | 把可观察版本作为 stamp 或换用合适的不可变状态 |
| 读取引用与版本 | get(int[]) 返回配对快照 | 不要拆成两次读取后再假定它们属于同一时刻 |
| 长期运行的版本计数 | stamp 只是 int 标记 | 设计溢出、回绕和重试策略,不能把它当无限大的序号 |
还要注意引用比较是身份比较,不是调用对象的 equals。如果业务需要比较对象内容,应该先把状态设计成适合原子替换的不可变对象,再把该对象引用和 stamp 作为整体更新。
相关问题
stamp 不变但引用变了,CAS 会成功吗?
不会。compareAndSet 要求当前引用身份和当前 stamp 同时满足期望值;任意一项不匹配都会返回 false。
能不能只调用 getReference() 检测 ABA?
不能可靠检测。它只能告诉你当前引用是谁,不能说明引用在读取期间是否经历过其他状态。检测 ABA 至少要把引用和独立 stamp 作为配对快照保存。
为什么不直接把 stamp 当成对象版本?
stamp 只是一个并发判定标记,类型是 int,不会自动跟踪对象内部字段。只有当更新协议明确规定“哪些变化必须更新 stamp”时,它才有检测意义。
最后回到标题:用 AtomicStampedReference 检测 ABA,先通过 get(int[]) 保存引用与 stamp 的配对快照,再用 compareAndSet 同时校验引用身份和 stamp。引用即使回到 A,只要 stamp 已从旧值变化,旧快照就不会被误认为仍然有效。