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。上面的例子没有手动释放内存,但同一个对象引用依然可以被移走再放回来。

最后修改:2018 年 02 月 12 日
如果觉得我的文章对你有用,请随意赞赏