基於atomic包分析CAS原理

大家都知道,多線程下操作共享變量,會出現所謂的“線程安全問題”從而不能得到我們預期的結果,爲了解決這種問題,在早期的JDK版本中,提供的synchronized關鍵字來解決這種線程安全問題,而在JDK1.5以後的java.util.concurrent包中,裏面大量使用了一種叫CAS的技術,提供了一種不用synchronized的前提下解決線程安全問題的方案。本文將從AtomicInteger包入手,講解CAS的原理和使用、以及CAS可能出現的問題

先來一個小demo

不想上來就貼上來千篇一律的各種概念,懂得人我不說也懂,不懂得看完概念依然不懂,用碼說話,先看個小demo,開五個線程,每個線程累計1000次操作共享變量,共享變量分別使用int和基於CAS的AtomicInteger

public class AtomicTest {

    int count = 0;
    AtomicInteger atomicInteger = new AtomicInteger(0);

    // 加1操作
    public void add() {
        count++;
        atomicInteger.addAndGet(1);
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicTest atomicTest = new AtomicTest();
        List<Thread> threads = new ArrayList<>();
        // 開5個線程
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                // 每個線程執行1000次add方法
                for (int num = 0; num < 1000; num++) {
                    atomicTest.add();
                }
            });
            threads.add(thread);
            thread.start();
        }
        // 保證上面開啓的五個線程執行在main線程之前
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("count-->" + atomicTest.count);
        System.out.println("atomic-->" + atomicTest.atomicInteger);
    }
}

結果可能每次都不一樣,但是使用AtomicInteger的值每次必是5000

count-->3670
atomic-->5000

Process finished with exit code 0

點進源碼看一下

(基於JDK1.8,不用版本可能稍有不同)
點進atomicInteger.addAndGet(1);

/**
     * Atomically adds the given value to the current value.
     *
     * @param delta the value to add
     * @return the updated value
     */
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

只有一行代碼,然後調用了unsafe的getAndAddInt,傳參分別是當前對象引用,valueOffset和我們要增加的值delta,valueOffset我們一會詳細說一下,現在先點到unsafe.getAndAddInt(this, valueOffset, 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;
    }

我們看到,進入了一個叫Unsafe的類中,首先先調取this.getIntVolatile(var1, var2);,這是一個native方法,通過傳入的var1(也就是上面AtomicInteger的this)和var2,從內存中獲取到最新的值,然後執行compareAndSwapInt(var1, var2, var5, var5 + var4)方法,就是我們的主角CAS(Compare and Swap),依然是native方法,實現原理是拿當前獲取到的值(var5)通內存的值比較,如果值相同,則將內存的值更新爲新的值(var5+var4,var4也就是我們上面傳入的delta),如果與內存中的值不同,則繼續執行do中的邏輯,獲取最新的var5並繼續執行compareAndSwap操作,直至成功。
其實CAS直至JDK1.5才被廣泛使用的原因是,CAS是需要硬件支持的,隨着處理器的發展,逐漸提供了將一系列操作原子操作的指令,其中CAS底層使用的就是CMPXCHG指令。
回到上面這段代碼,看到最後return的值爲var5,並不是執行操作後最新的值,當返回var5後,addAndGet(int delta)中,再加上delta,其實大家想一想,這樣操作,雖然能保證AtomicInteger內部每次操作都是原子性的,卻不能保證每次調用addAndGet方法返回的數據是順序的,換句話說,它能保證結果的最終一致性,但是並不能保證每條線程中間狀態的一致性,這也是爲什麼atomic包的類適合計數,但並不適合做例如生成訂單號等要求強順序的業務操作。

重點說下valueOffset

回過頭來,看下剛剛還沒解釋的valueOffset,是個啥。valueOffset,存於AtomicInteger的成員變量中,在靜態代碼塊中初始化

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

然後整個AtomicInteger中並沒有修改它的地方。說明這個值在整個JVM中是唯一的,一個唯一的值,還被頻繁傳到各種native方法中,有什麼意義的?當我們創建了一個對象之後,在內存中開闢了一片空間,但是這個對象的這片空間中,也是存放了各種數據的,這些對象怎麼存放樣式,被稱作“佈局”,而這個valueOffset,就是獲取的一個字段在這個佈局中的一個相對位置,叫着“偏移量”,根據上面代碼我們看到獲取的是value,這樣,通過當前對象和value的偏移量,可以在內存中快速的定位到value的值。

說到 CAS不得不說的ABA問題

CAS看起來不會有任何問題,完美解決了多線程下數據安全問題,但是可能有這樣一個場景

線程1:
獲取內存中的值,爲A
然後被掛起
線程2
獲取內存中的值,爲A
修改A爲B
修改B爲A
執行完畢
線程1:
獲取到的值A與內存中比對一致,可以操作

上述場景就出現了ABA問題,關於ABA問題會造成的問題,感興趣的小夥伴可以查一查深入瞭解,要是基於我們正常使用的常用操作,能保證結果的正確,並不會對我們造成什麼影響。如果非要規避ABA的場合,可以使用AtomicStampedReference,通過內部維護的一個時間戳來保證,比如上面線程2的操作,可理解爲

修改A爲B(2B)
修改B爲A(3A)

此時線程1執行,獲取到的A與當前內存中的3A就有差異了。
雖然能規避ABA問題,但是額外的開銷勢必會影響性能,而使用這種規避ABA問題的CAS實現,性能有沒有使用synchronized高呢?可以根據實際業務場景和配置,做一個性能對比來權衡。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章