CAS 能判断当前值是不是预期值,却不能判断它中间有没有被修改过。值从 A 变成 B,再变回 A,比较仍然可能成功。
重现 A 到 B 再到 A
import java.util.concurrent.atomic.AtomicReference;
public class AbaDemo {
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
AtomicReference<Object> ref = new AtomicReference<>(a);
Object expected = ref.get();
Thread changer = new Thread(() -> {
ref.compareAndSet(a, b);
ref.compareAndSet(b, a);
});
changer.start();
changer.join();
System.out.println(ref.compareAndSet(expected, new Object()));
}
}输出 true。主线程保存了最初的引用,另一个线程做完两次修改后,当前引用又是同一个 a。最后的 CAS 无法发现这段历史。
这里用 join() 固定两次修改已经完成,不需要靠休眠碰运气。AtomicReference 比较的是引用身份,而不是调用对象的 equals()。
带上版本号
AtomicStampedReference 同时保存引用和一个整数标记。读取时把二者一起取出,更新时也一起比较:
import java.util.concurrent.atomic.AtomicStampedReference;
public class StampedReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object a = new Object();
Object b = new Object();
AtomicStampedReference<Object> ref = new AtomicStampedReference<>(a, 0);
int[] stamp = new int[1];
Object expected = ref.get(stamp);
int expectedStamp = stamp[0];
Thread changer = new Thread(() -> {
ref.compareAndSet(a, b, 0, 1);
ref.compareAndSet(b, a, 1, 2);
});
changer.start();
changer.join();
System.out.println(ref.compareAndSet(expected, new Object(),
expectedStamp, expectedStamp + 1));
}
}这次输出 false。引用虽然恢复了,版本号却已经不是最初的 0。
版本号不是自动递增的,需要所有修改者遵守同一套更新规则。读取引用和版本号时使用 get(int[]),可以取得同一次快照;分两次调用 getter,可能读到不同更新阶段的组合。
标记与版本的区别
AtomicMarkableReference 保存的是引用和布尔标记,适合表示某个节点是否被逻辑删除。布尔值只有两种状态,如果反复从 false 改到 true 再改回 false,它本身也不能记录修改次数。
并不是每次 ABA 都有问题。如果业务只在意当前值是否等于 A,中间的修改可能无关紧要;如果依赖“自从读出以后从未改变过”,就需要版本信息或其他同步协议。
Java 的垃圾回收也不会自动替你消除逻辑上的 ABA。上面的例子没有手动释放内存,但同一个对象引用依然可以被移走再放回来。