原子類的ABA問題


原子類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在進行操作的時候,就出現操作失敗了,因為版本號和當初拿到的不一樣


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM