21 如何通过Atomic类解决ABA问题
Diego38 51 1

1. 前言

CAS 算法采用比较并交换,原值与预期值相等则替换目标值,实际上是一种乐观锁,能满足绝大多数的业务场景。

但是有一种更加严格的场景,CAS 算法能够执行成功,但不符合预期。这就是本节我们介绍的 ABA 问题。

2. 什么是 ABA 问题

CAS 是对内存的变量 V,提供一个旧值 A 和新值 B,如果变量 V 与旧值 A 相等,则将 V 更新为新值 B,并且返回成功,否则更新失败。

所谓 ABA 问题是指如果一个线程将原值为 A 的变量,修改为 B,然后再次改回 A,CAS 操作是无法感知当前变量 V 是否曾经发生过变化的。举个例子,小明骑自行车去学校上课,白天自行车锁在校门口,而小偷把自行车开锁后办完事情又重新骑回来锁好,等小明放学后回来发现锁完好无损,就骑车回家了。实际上自行车锁已经不安全了。

要解决此类问题,需要对变量附加一个版本号,每次修改可以对版本号递增。

3. 使用 AtomicMarkableReference 和 AtomicMarkableReference 解决 ABA 问题

Atomic 包中提供了两个解决 ABA 的类一个是 AtomicMarkableReference,以及 AtomicMarkableReference。

我们看下代码:

public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

从 AtomicStampedReference 可以看出,内部变量不再是单纯的一个变量,而是一个包含版本号 stamp 的 Pair。每次修改变量 reference,我们可以指定原来的版本号 stamp,以及修改成功后的 stamp。

AtomicMarkableReference 是 AtomicStampedReference 的简化版,将 stamp 替换成了 mark。

看一个解决 ABA 问题的代码样例:

public class AtomicStampedReferenceTest {

    public static void main(String[] args) {
        AtomicStampedReference<Long> reference = new AtomicStampedReference(10L,1);
        System.out.println("初始值:"+ reference.getReference().intValue());
        System.out.println("初始版本号:"+reference.getStamp());
        boolean b = reference.compareAndSet(10L, 20L, 1, 2);
        System.out.println(b ? "修改成功"  : "修改失败" );

        b = reference.compareAndSet(20L, 30L, -1, 2);
        System.out.println(b ? "修改成功"  : "修改失败" );
    }
}

输出如下:

初始值:10
初始版本号:1
修改成功
修改失败

代码演示了通过传递版本号进行 CAS 执行的过程,当传递正确预期值 + 正确版本号时 CAS 操作才会成功,否则就会失败,成功解决了 ABA 问题。

4. 总结

AtomicStampedReference 和 AtomicMarkableReference 是专门为解决 ABA 问题设计的,对内置的变量赋予版本号,版本号记录了修改的痕迹,让 ABA 问题无所遁形。

预览图
评论区

索引目录