ABA 问题

所谓的 ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了,这就是 ABA 问题。

比如:张三去银行取钱,余额有 200 元,张三取 100 元,但因为程序的问题,启动了两个线程,线程一和线程二进行比对扣款,线程一获取原本有 200 元,扣除 100 元,余额等于 100 元,此时李四给张三转账 100 元,于是启动了线程三抢先在线程二之前执行了转账操作,把 100 元又变成了 200 元,而此时线程二对比自己事先拿到的 200 元和此时经过改动的 200 元值一样,就进行了减法操作,把余额又变成了 100 元。这显然不是我们要的正确结果,我们想要的结果是余额减少了 100 元,又增加了 100 元,余额还是 200 元,而此时余额变成了 100 元,显然有悖常理,这就是著名的 ABA 的问题。

执行流程如下:

  • 线程一:取款,获取原值 200 元,与 200 元比对成功,减去 100 元,修改结果为 100 元。
  • 线程二:取款,获取原值 200 元,阻塞等待修改。
  • 线程三:转账,获取原值 100 元,与 100 元比对成功,加上 100 元,修改结果为 200 元。
  • 线程二:取款,恢复执行,原值为 200 元,与 200 元对比成功,减去 100 元,修改结果为 100 元。

最终的结果是 100 元。

解决 ABA

解决 ABA 问题的一种方法是使用带版本号的 CAS,也称为双重 CAS(Double CAS)或者版本号 CAS。具体来说,每次进行 CAS 操作时,不仅需要比较要修改的内存地址的值与期望的值是否相等,还需要比较这个内存地址的版本号是否与期望的版本号相等。如果相等,才进行修改操作。这样,在修改后的值后面追加上一个版本号,即使变量的值从 A 变成了 B 再变成了 A,版本号也会发生变化,从而避免了误判。

以下是一个使用 AtomicStampedReference 来解决 ABA 问题的示例代码:

import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {

    private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(1, 0);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("初始值:" + atomicStampedRef.getReference() + ",版本号:" + atomicStampedRef.getStamp());

        // 线程 1 先执行一次 CAS 操作,期望值为 1,新值为 2,版本号为 0
        Thread thread1 = new Thread(() -> {
            int stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1);
        });

        // 线程 2 先 sleep 1 秒,让线程 1 先执行一次 CAS 操作,然后再执行一次 CAS 操作,期望值为 2,新值为 1,版本号为 1
        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            int stamp = atomicStampedRef.getStamp();
            atomicStampedRef.compareAndSet(2, 1, stamp, stamp + 1);
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("最终值:" + atomicStampedRef.getReference() + ",版本号:" + atomicStampedRef.getStamp());
    }
}

以上程序的执行结果为:

初始值:1,版本号:0

最终值:1,版本号:2

从输出结果可以看出,即使变量的值从 1 变成了 2 再变成了 1,使用带版本号的 CAS 操作也能正确判断变量是否发生了变化。


以上内容来自我的 《Java 面试突击训练营》,这门课程是 有着 14 年工作经验(前 360 开发工程师),9 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课

整个课程从 Java 基础到微服务 Spring Cloud、从实际开发问题到场景题应有尽有,如下图所示:

全程通过视频直播 + 录播的方式,把 Java 常见的面试题系统的过一遍,遇到一个问题,把这个问题相关的内容都给大家讲明白,并且视频支持永久更新和观看。

上完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,想要了解详情,加我微信:vipStone【备注:训练营】