原子類的ABA問題

MXC肖某某發表於2020-04-25

原子類AtomicInteger的ABA問題

連環套路

從AtomicInteger引出下面的問題

CAS -> Unsafe -> CAS底層思想 -> ABA -> 原子引用更新 -> 如何規避ABA問題

ABA問題是什麼

狸貓換太子

假設現在有兩個執行緒,分別是T1 和 T2,然後T1執行某個操作的時間為10秒,T2執行某個時間的操作是2秒,最開始AB兩個執行緒,分別從主記憶體中獲取A值,但是因為B的執行速度更快,他先把A的值改成B,然後在修改成A,然後執行完畢,T1執行緒在10秒後,執行完畢,判斷記憶體中的值為A,並且和自己預期的值一樣,它就認為沒有人更改了主記憶體中的值,就快樂的修改成B,但是實際上 可能中間經歷了 ABCDEFA 這個變換,也就是中間的值經歷了狸貓換太子。

所以ABA問題就是,在進行獲取主記憶體值的時候,該記憶體值在我們寫入主記憶體的時候,已經被修改了N次,但是最終又改成原來的值了

CAS導致ABA問題

CAS演算法實現了一個重要的前提,需要取出記憶體中某時刻的資料,並在當下時刻比較並替換,那麼這個時間差會導致資料的變化。

比如說一個執行緒one從記憶體位置V中取出A,這時候另外一個執行緒two也從記憶體中取出A,並且執行緒two進行了一些操作將值變成了B,然後執行緒two又將V位置的資料變成A,這時候執行緒one進行CAS操作發現記憶體中仍然是A,然後執行緒one操作成功

儘管執行緒one的CAS操作成功,但是不代表這個過程就是沒有問題的

ABA問題

CAS只管開頭和結尾,也就是頭和尾是一樣,那就修改成功,中間的這個過程,可能會被人修改過

原子引用

原子引用其實和原子包裝類是差不多的概念,就是將一個java類,用原子引用類進行包裝起來,那麼這個類就具備了原子性

/**
 * 原子類引用
 */
@Data
@AllArgsConstructor
class User {
    String userName;
    int age;
}
public class AtomicReferenceDemo {

    public static void main(String[] args) {

        User aaa = new User("aaa", 20);
        User bbb = new User("bbb", 30);

        // 建立原子引用包裝類
        AtomicReference<User> atomicReference = new AtomicReference<>();
        // 現在主實體記憶體的共享變數,為aaa
        atomicReference.set(aaa);

        // 比較並交換,如果現在主實體記憶體的值為aaa,那麼交換成bbb
        System.out.println(atomicReference.compareAndSet(aaa, bbb) + "\t " + atomicReference.get().toString());
        // 比較並交換,現在主實體記憶體的值是bbb了,但是預期為aaa,因此交換失敗
        System.out.println(atomicReference.compareAndSet(aaa, bbb) + "\t " + atomicReference.get().toString());
    }
}

基於原子引用的ABA問題

我們首先建立了兩個執行緒,然後T1執行緒,執行一次ABA的操作,T2執行緒在一秒後修改主記憶體的值

/**
 * 基於CAS引出ABA問題
 */
public class ABADemo {

    static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);

    public static void main(String[] args) {
        new Thread(()->{
            // 把100 改成 127 然後在改成100,也就是ABA
            atomicReference.compareAndSet(100, 127);
            //特別強調在AtomicReference(Integer)中value超出-128~127,會生成一個新的物件而造成無法修改
            //但是在AtomicInteger中則不會存在這樣的問題
            atomicReference.compareAndSet(127, 100);
        },"t1").start();

        new Thread(()->{
            try {
                // 睡眠一秒,保證t1執行緒,完成了ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 把100 改成 127 然後在改成100,也就是ABA
            System.out.println(atomicReference.compareAndSet(100, 2021)+"\t"+atomicReference.get());
        },"t2").start();
    }
}

我們發現,它能夠成功的修改,這就是ABA問題

解決ABA問題

新增一種機制,也就是修改版本號,類似於時間戳的概念

T1: 100 1 2020 2

T2: 100 1 127 2 100 3

如果T1修改的時候,版本號為2,落後於現在的版本號3,所以要重新獲取最新值,這裡就提出了一個使用時間戳版本號,來解決ABA問題的思路

AtomicStampedReference

時間戳原子引用,來這裡應用於版本號的更新,也就是每次更新的時候,需要比較期望值和當前值,以及期望版本號和當前版本號

/**
 * 基於CAS引出ABA問題並採用AtomicStampedReference解決
 */
public class ABADemo {

    static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);

    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        System.out.println("============以下是ABA問題的產生==========");

        new Thread(() -> {
            // 把100 改成 101 然後在改成100,也就是ABA
            atomicReference.compareAndSet(100, 127);
            atomicReference.compareAndSet(127, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                // 睡眠一秒,保證t1執行緒,完成了ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 把100 改成 101 然後在改成100,也就是ABA
            System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());

        }, "t2").start();

        //main執行緒和gc執行緒之外如果還有執行緒就處於等待
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("============以下是ABA問題的解決==========");

        new Thread(() -> {

            // 獲取版本號
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp);

            // 暫停t3一秒鐘
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 傳入4個值,期望值,更新值,期望版本號,更新版本號
            atomicStampedReference.compareAndSet(100, 127, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "\t 第二次版本號" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(127, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "\t 第三次版本號" + atomicStampedReference.getStamp());

        }, "t3").start();

        new Thread(() -> {

            // 獲取版本號
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp);

            // 暫停t4 3秒鐘,保證t3執行緒也進行一次ABA問題
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp+1);

            System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 當前最新實際版本號:" + atomicStampedReference.getStamp());

            System.out.println(Thread.currentThread().getName() + "\t 當前實際最新值" + atomicStampedReference.getReference());


        }, "t4").start();
    }
}

執行結果為:

我們能夠發現,執行緒t3,在進行ABA操作後,版本號變更成了3,而執行緒t4在進行操作的時候,就出現操作失敗了,因為版本號和當初拿到的不一樣

相關文章