ABA问题、AtomicStampReference

914 阅读5分钟

什么是ABA问题

在多线程场景下CAS(比较并交换)会出现ABA问题,比如当有两个线程同时去修改变量的值,线程1和线程2都将变量由A改为B。首先线程1获得CPU的时间片,线程2由于某些原因被挂起,线程1经过CAS(Comparent and Swap)将变量的值从A改为B,线程1更新完变量的值后,此时恰好有线程3进来了,线程3通过CAS(comparent And Swap)将变量的值由B改为A,线程3更新完成后,线程2获取时间片继续执行,通过CAS(comparent And Swap)将变量的值由A改为B,而此时的线程2并不知道该变量已经有了A->B->A改变的过程。这就是CAS中的ABA问题。

如何避免ABA问题

  • 通过加版本号(每次变量更新的时候,将当前变量的版本号加1)或者时间戳解决,或者保证单向递增或递减不会存在此类问题。Atmoic包下的AtmoicStampedReference类,其compareAndSet()方法首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将当前引用更新为新引用(AtomicMarkableReference也可以用来解决CAS中的ABA问题,与AtomicStampReference不同的是其版本号的数据类型为boolean)。

ABA问题再现

使用AtomicInteger无法发现ABA问题

package com.gjy.demo.Atomic;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author GJY
 * @Date 2021/7/27 20:35
 * @Version 1.0
 */
public class AtomicInteger_ABADemo {

    private static AtomicInteger at = new AtomicInteger(1);
    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = at.get();
                System.out.println("线程A读取初始值:"+i);//1
                //比较并交换,将at的值从1修改为2
                at.compareAndSet(at.get(),2);
                //比较并交换,将at的值从2修改为1
                at.compareAndSet(at.get(),1);
            }
        }, "A线程");

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = at.get();
                System.out.println("线程B读取初始值:"+i);//1
                try {
                    //线程B调用线程A的join方法,线程B会阻塞直到线程A执行完毕。
                    threadA.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //比较并交换,将at的值由1修改回2
                boolean result = at.compareAndSet(i, 2);
                System.out.println("线程B比较并修改at的值的执行结果-> "+result);//true

            }
        }, "B线程");

        //启动线程
        threadA.start();
        threadB.start();

    }
}
运行结果:
线程A读取初始值:1
线程B读取初始值:1
线程B比较并修改at的值的执行结果-> true

使用AtomicStampReference可以发现ABA问题

package com.gjy.demo.Atomic;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @Author GJY
 * @Date 2021/7/27 20:48
 * @Version 1.0
 */
public class AtomicStampReference_ABADemo {
    //初始版本号为0
    private static AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(1, 0);

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            try {
                //线程A睡眠1秒,此时睡眠期间线程B会获得初始版本号0
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //获取版本号
            System.out.println("线程A执行前获取的版本号:" + asr.getStamp());
            //线程A比较期望值和版本号并将asr从1改为2
            asr.compareAndSet(1, 2, asr.getStamp(), asr.getStamp() + 1);
            //线程A比较期望值和版本号并将asr从2改回为1
            asr.compareAndSet(2, 1, asr.getStamp(), asr.getStamp() + 1);
            System.out.println("线程A执行两次修改后的版本号:" + asr.getStamp());
        }, "线程A");

        Thread threadB = new Thread(() -> {
            //线程B获取版本号0,因为线程A开始执行时先睡眠1秒,然后再执行比较并交换
            int stamp = asr.getStamp();
            System.out.println("线程B获取的版本号:"+stamp);//0
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //线程B比较期望值和版本号并将asr从1改为2
            boolean result = asr.compareAndSet(1,
                                    2,
                                            stamp,
                                    stamp + 1);
            System.out.println("线程B比较并修改asr的值的执行结果-> "+result);
            System.out.println("线程B执行CAS后的版本号:"+asr.getStamp());//2
        }, "线程B");

        threadA.start();
        threadB.start();
    }
}

运行结果:
线程B获取的版本号:0
线程A执行前获取的版本号:0
线程A执行两次修改后的版本号:2
线程B比较并修改asr的值的执行结果-> false
线程B执行CAS后的版本号:2

AtomicStampReference源码

AtomicStampReference类的属性

//volatile修饰的pair
private volatile Pair<V> pair;

AtomicStampReference类的构造方法

/**
  * V initialRef :任意类型的初始引用对象
  * int initialStamp :Int类型的初始版本号
  */
public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

AtomicStampReference的内部类Pair

  • AtomicStampReference的内部类Pair维护了带有版本的Pair对象,Pair源码如下:
private static class Pair<T> {
    //引用
    final T reference;
    //版本号
    final int stamp;
    //构造方法
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    //生成一个Pair
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

comparedAndSwap()方法


/**
 * @param expectedReference 期望的引用值
 * @param newReference 新的引用值
 * @param expectedStamp 期望的版本号
 * @param newStamp 新的版本号
 * @return {@code true} if successful 更新成功返回true,失败返回false
 */

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference && newStamp == current.stamp) 
        || 
        casPair(current, Pair.of(newReference, newStamp))
        );
}
  • 具体执行流程:
  1. 若原引用不等于期望引用,则方法返回false,否则执行第二步
  2. 若原引用等于新引用并且原版本号与期望版本号不一致,则返回false,否则执行第三步
  3. 若原引用与新引用相等并且原版本号与新版本号一致,则无需修改引用和版本号,否则,调用casPair()方法进行原子性更新引用和版本号。

image.png

以上对ABA问题、AtomicStampReference的简单介绍。在我们的业务开发中经常会出现ABA问题类似的场景,但又往往不容易被我们开发者所发现,往往等到测试的时候才被发现,所以正确认识ABA问题、掌握解决ABA问题的方法,可以减少我们业务的Bug。 默认标题_动态分割线_2021-07-15-0.gif 往期推荐