深入分析CAS

CAS,Compare And Swap,即比較並交換。Doug lea大神在同步組件中大量使用CAS技術鬼斧神工地實現了Java多線程的併發操作。整個AQS同步組件、Atomic原子類操作等等都是以CAS實現的,甚至ConcurrentHashMap在1.8的版本中也調整爲了CAS+Synchronized。可以說CAS是整個JUC的基石。 
這裏寫圖片描述

CAS分析

在CAS中有三個參數:內存值V、舊的預期值A、要更新的值B,當且僅當內存值V的值等於舊的預期值A時纔會將內存值V的值修改爲B,否則什麼都不幹。其僞代碼如下:

if(this.value == A){
    this.value = B
    return true;
}else{
    return false;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

JUC下的atomic類都是通過CAS來實現的,下面就以AtomicInteger爲例來闡述CAS的實現。如下:

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

Unsafe是CAS的核心類,Java無法直接訪問底層操作系統,而是通過本地(native)方法來訪問。不過儘管如此,JVM還是開了一個後門:Unsafe,它提供了硬件級別的原子操作。

valueOffset爲變量值在內存中的偏移地址,unsafe就是通過偏移地址來得到數據的原值的。

value當前值,使用volatile修飾,保證多線程環境下看見的是同一個。

我們就以AtomicInteger的addAndGet()方法來做說明,先看源代碼:

    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

內部調用unsafe的getAndAddInt方法,在getAndAddInt方法中主要是看compareAndSwapInt方法:

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  • 1
  • 1

該方法爲本地方法,有四個參數,分別代表:對象、對象的地址、預期值、修改值(有位夥伴告訴我他面試的時候就問到這四個變量是啥意思…+_+)。該方法的實現這裏就不做詳細介紹了,有興趣的夥伴可以看看openjdk的源碼。

CAS可以保證一次的讀-改-寫操作是原子操作,在單處理器上該操作容易實現,但是在多處理器上實現就有點兒複雜了。

CPU提供了兩種方法來實現多處理器的原子操作:總線加鎖或者緩存加鎖。

總線加鎖:總線加鎖就是就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔使用共享內存。但是這種處理方式顯得有點兒霸道,不厚道,他把CPU和內存之間的通信鎖住了,在鎖定期間,其他處理器都不能其他內存地址的數據,其開銷有點兒大。所以就有了緩存加鎖。

緩存加鎖:其實針對於上面那種情況我們只需要保證在同一時刻對某個內存地址的操作是原子性的即可。緩存加鎖就是緩存在內存區域的數據如果在加鎖期間,當它執行鎖操作寫回內存時,處理器不在輸出LOCK#信號,而是修改內部的內存地址,利用緩存一致性協議來保證原子性。緩存一致性機制可以保證同一個內存區域的數據僅能被一個處理器修改,也就是說當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。

CAS缺陷

CAS雖然高效地解決了原子操作,但是還是存在一些缺陷的,主要表現在三個方法:循環時間太長、只能保證一個共享變量原子操作、ABA問題。

循環時間太長

如果CAS一直不成功呢?這種情況絕對有可能發生,如果自旋CAS長時間地不成功,則會給CPU帶來非常大的開銷。在JUC中有些地方就限制了CAS自旋的次數,例如BlockingQueue的SynchronousQueue。

只能保證一個共享變量原子操作

看了CAS的實現就知道這隻能針對一個共享變量,如果是多個共享變量就只能使用鎖了,當然如果你有辦法把多個變量整成一個變量,利用CAS也不錯。例如讀寫鎖中state的高地位

ABA問題

CAS需要檢查操作值有沒有發生改變,如果沒有發生改變則更新。但是存在這樣一種情況:如果一個值原來是A,變成了B,然後又變成了A,那麼在CAS檢查的時候會發現沒有改變,但是實質上它已經發生了改變,這就是所謂的ABA問題。對於ABA問題其解決方案是加上版本號,即在每個變量都加上一個版本號,每次改變時加1,即A —> B —> A,變成1A —> 2B —> 3A。

用一個例子來闡述ABA問題所帶來的影響。

有如下鏈表

這裏寫圖片描述

假如我們想要把B替換爲A,也就是compareAndSet(this,A,B)。線程1執行B替換A操作,線程2主要執行如下動作,A 、B出棧,然後C、A入棧,最終該鏈表如下:

這裏寫圖片描述

完成後線程1發現仍然是A,那麼compareAndSet(this,A,B)成功,但是這時會存在一個問題就是B.next = null,compareAndSet(this,A,B)後,會導致C丟失,改棧僅有一個B元素,平白無故把C給丟失了。

CAS的ABA隱患問題,解決方案則是版本號,Java提供了AtomicStampedReference來解決。AtomicStampedReference通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題。對於上面的案例應該線程1會失敗。

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)));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

compareAndSet有四個參數,分別表示:預期引用、更新後的引用、預期標誌、更新後的標誌。源碼部門很好理解預期的引用 == 當前引用,預期的標識 == 當前標識,如果更新後的引用和標誌和當前的引用和標誌相等則直接返回true,否則通過Pair生成一個新的pair對象與當前pair CAS替換。Pair爲AtomicStampedReference的內部類,主要用於記錄引用和版本戳信息(標識),定義如下:

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

Pair記錄着對象的引用和版本戳,版本戳爲int型,保持自增。同時Pair是一個不可變對象,其所有屬性全部定義爲final,對外提供一個of方法,該方法返回一個新建的Pari對象。pair對象定義爲volatile,保證多線程環境下的可見性。在AtomicStampedReference中,大多方法都是通過調用Pair的of方法來產生一個新的Pair對象,然後賦值給變量pair。如set方法:

    public void set(V newReference, int newStamp) {
        Pair<V> current = pair;
        if (newReference != current.reference || newStamp != current.stamp)
            this.pair = Pair.of(newReference, newStamp);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

下面我們將通過一個例子可以可以看到AtomicStampedReference和AtomicInteger的區別。我們定義兩個線程,線程1負責將100 —> 110 —> 100,線程2執行 100 —>120,看兩者之間的區別。

public class Test {
    private static AtomicInteger atomicInteger = new AtomicInteger(100);
    private static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);

    public static void main(String[] args) throws InterruptedException {

        //AtomicInteger
        Thread at1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.compareAndSet(100,110);
                atomicInteger.compareAndSet(110,100);
            }
        });

        Thread at2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(2);      // at1,執行完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("AtomicInteger:" + atomicInteger.compareAndSet(100,120));
            }
        });

        at1.start();
        at2.start();

        at1.join();
        at2.join();

        //AtomicStampedReference

        Thread tsf1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //讓 tsf2先獲取stamp,導致預期時間戳不一致
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 預期引用:100,更新後的引用:110,預期標識getStamp() 更新後的標識getStamp() + 1
                atomicStampedReference.compareAndSet(100,110,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
                atomicStampedReference.compareAndSet(110,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);
            }
        });

        Thread tsf2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedReference.getStamp();

                try {
                    TimeUnit.SECONDS.sleep(2);      //線程tsf1執行完
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("AtomicStampedReference:" +atomicStampedReference.compareAndSet(100,120,stamp,stamp + 1));
            }
        });

        tsf1.start();
        tsf2.start();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

運行結果:

這裏寫圖片描述

運行結果充分展示了AtomicInteger的ABA問題和AtomicStampedReference解決ABA問題。

發佈了177 篇原創文章 · 獲贊 405 · 訪問量 94萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章