CAS是什麼?ABA問題又應該如何理解?

一、爲什麼值和預期不一樣?

​ 我們先來看下下面的這段 Java 程序,開啓十個線程,每個線程進行 number++ 操作 1000 次,最終輸出的值大小應該爲 10000:

public void addNumber(){
    number++;
}

for (int i = 1; i <=10 ; i++) {
    new Thread(()->{
        for (int j = 1; j <=1000; j++) {
            data.addNumber();
        }
    },String.valueOf(i)).start();
}

​ 當我們打印出最終的 number 的值的時候發現,每一次的值都小於預期的 10000 。這是因爲 number++ 並非是一步操作,當執行它時會分爲三條指令:① 獲取到原始值;② 對原始值進行加一操作得到新值;③ 將新值寫回內存。在併發較高的情況下,當兩個線程同時獲取到舊的值之後,就會產生寫入的值相同的狀況,造成總和總比預期值小的後果。

​ 爲了解決這個問題,我們可以給 addNumber() 方法加上 synchronized 修飾解決。synchronized 屬於悲觀鎖,一次只允許一個線程進行 number++的操作,雖然這樣能夠解決併發問題,但是在此處的效率並不高。由此我們可以使用一種樂觀鎖,樂觀鎖的含義是假設沒有發生衝突,那麼就正好可以進行某項操作,如果要是發生衝突了,那就重試直到成功,其最常見的就是CAS 。對 addNumber()方法進行以下修改便可以避免併發值重複的問題。

AtomicInteger atomicInteger =new AtomicInteger();
public void addNumber(){
    atomicInteger.getAndIncrement();
}

​ 這裏邊使用到了java.util.concurrent.atomic包下的AtomicInteger來解決原子性問題。

二、CAS是什麼?

​ 在上面我們使用到了 Java 的java.util.concurrent.atomic包,這個包便借用了 CAS 來實現了區別於synchronized 同步鎖的一種樂觀鎖。

​ 那麼 CAS 是什麼呢?CAS其實就是Compare And Swap 的簡寫,它是一條 CPU 併發原語。原語的執行必須是連續的,在執行過程中不允許中斷,也就是說CAS是一條原子指令,不會造成所謂的數據不一致的問題。我們跟蹤atomicInteger.getAndIncrement();這條語句可以得到以下的內容:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

​ 由此可以發現它的實現藉助了一個叫做Unsafe的類。Unsafe類是 Java 中用於直接操作內存數據的一個類(類似於C語言中的指針操作),其中包含很多的本地方法(native)。

​ 以getAndAddInt(Object var1, long var2, int var4)方法爲例,展示其實如何實現的保證原子性。方法的實現如下:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

​ 該方法中包含三個參數,分別代表的意思是:

​ ① 當前對象;

​ ② 該變量值在內存中的偏移地址;

​ ③ 需要增加的值大小。

​ 在這段代碼中,有一個叫做compareAndSwapInt的方法,public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);,該方法共包含四個參數,返回值爲布爾型,使用當前對象的當前值與 var5 進行比較,如果相同則更新值返回 true,失敗返回 false,參數含義如下。

​ Object var1:代表當前對象;

​ long var2:代表內存偏移量,相當於對象值的引用地址;

​ int var4:代表期望值,使用期望值和當前對象中的值進行比較;

​ int var5:代表要交換的值。

​ 該方法中使用了自旋鎖以保證其原子性。假設主內存值爲 v 等於10,此時有 T1、T2兩個線程進入到該方法,根據 Java 內存模型(JMM)我們可以知道,線程 T1 和線程 T2 都會將主內存的值10拷貝到自己的工作內存。

​ 1、當線程 T1 和線程 T2 都通過getIntVolatile(var1, var2)賦值給了變量 var5 之後,線程 T1 被掛起;

​ 2、線程 T2 調用方法compareAndSwapInt,因爲當中的期望值 var5 和當前主內存值相同,比較成功,更新當前內存的值爲 11,返回 true,退出循環;

​ 3、線程 T1 被喚醒,在執行compareAndSwapInt方法的時候,由於當前內存的值以及爲11,和 工作內存 var5 的值10不同了,所以比較不成功,返回 false,繼續執行循環;

​ 4、線程 T1 重新從主內存獲取當前的最新值11賦值給 var5;

​ 5、線程 T1 繼續進行比較,若此時沒有其他線程對主內存的進行修改,比較更新成功 ,退出循環;否則繼續執行步驟4。

​ 流程圖如下所示:

​ 雖然CAS沒有加鎖保證了一致性,併發性有所提高 ,但是也產生了一系列的問題,比如循環時間長開銷大只能保證一個共享變量的原子操作會產生ABA問題

三、ABA 問題是什麼?

​ 使用 CAS 會產生 ABA 問題,這是因爲 CAS 算法是在某一時刻取出內存值然後在當前的時刻進行比較,中間存在一個時間差,在這個時間差裏就可能會產生 ABA 問題。

​ ABA 問題的過程是當有兩個線程 T1 和 T2 從內存中獲取到值A,線程 T2 通過某些操作把內存 值修改爲B,然後又經過某些操作將值修改爲回值A,T2退出。線程 T1 進行操作的時候 ,使用預期值同內存中的值比較,此時均爲A,修改成功退出。但是此時的A以及不是原先的A了,這就是 ABA 問題,如下圖。

四、如何解決 ABA 問題?

​ 解決這個問題可以使用添加版本號的方式。我們可以使用 Java 中的提供的類AtomicStampedReference進行操作,其中的compareAndSet方法如下:

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)));
}

​ 這個方法包含了四個參數,expectedReference代表的是期望被修改的值,newReference代表的是新的值,expectedStamp代表期望被修改的版本號,newStamp代表新的版本號。只有當預期值和均當前內存值相同時纔會修改成功。ABA 問題的完整示例以及解決代碼如下:

public class ABADemo {
    private static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stamp + ",值是" + stampedReference.getReference());
            //暫停1秒鐘t1線程
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
            stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stampedReference.getStamp() + ",值是" + stampedReference.getReference());
            System.out.println("線程t1已完成1次ABA操作~~~~~");
        }, "t1").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",版本號爲" + stamp + ",值是" + stampedReference.getReference());
            //線程2暫停3秒,保證線程1完成1次ABA
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = stampedReference.compareAndSet(100, 6666, stamp, stamp + 1);
            System.out.println("當前線程名稱:" + Thread.currentThread().getName() + ",修改成功否:" + result + ",最新版本號" +
                    stampedReference.getStamp() + ",最新的值:" + stampedReference.getReference());
        }, "t2").start();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章