LongAdder詳解以及底層原理分析

一、原子累加器
我們都知道,原子整型可以在線程安全的前提下做到累加功能,而今天介紹的LongAdder具有更好的性能

我們先來看原子累加器和原子整型做累加的對比使用:

    private static <T> void demo(Supplier<T> supplier, Consumer<T> action){
        T adder = supplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();
        for (int i = 0;i<40;i++){
            ts.add(new Thread(()->{
                for (int j = 0;j<500000;j++){
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t->t.start());
        ts.forEach(t->{
            try {
                t.join();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });
    }
 
    public static void main(String[] args) {
        for (int i = 0;i<5;i++){
            demo(()->new LongAdder(),longAdder -> longAdder.increment());
        }
        for (int i = 0;i<5;i++){
            demo(()->new AtomicLong(),atomicLong -> atomicLong.getAndIncrement());
        }
    }

 

通過上面的代碼運行我們可以清晰地看到,兩種方式都有效的產完成了累加的效果,但是明顯使用累加器的效率要更好,甚至要高出原子類型累加好幾倍。

現在,我們可以簡單地理解爲,原子累加器就是JAVA在併發編程下提供的有保障的累加手段。

二、LongAdder執行原理
LongAdder之所以性能提升這麼多,就是在有競爭時,設置多個累加單元,不同線程累加不同的累加單元,最後在將其彙總,減少了cas重試失敗,從而提高了性能。

簡單來說,累加器就是將需要做累加的共享變量,分成許多部分,時多個線程只累加自己的部分(這樣做既可以減少使用普通整型容易出現的線程不安全錯誤,也可以提高原子類型在累加時效率底下的問題),足以後再將結果彙總,得到累加結果。

就比如銀行要清點大量鈔票,一個人來清點效率低下,所以需要多個人來(多個線程),將大量鈔票分給多個員工(分配累加單元),每個員工僅僅需要對自己的那部分鈔票清點(每個線程累加自己的累加單元),最後將結果彙總起來,就是所有鈔票的總數。

LongAdder中有幾個關鍵域:

transient volatile Cell[] cells;//累加單元數組,懶惰初始化
transient volatile long base;//基礎值,如果是單線程沒有競爭,則用cas累加這個域
transient volatile int cellsBusy;//zaicell創建或擴容時,置位1,表示加鎖


其中cells就是累加單元,用來給各個線程分配累加任務。

base用來做單線程的累加,同時還有彙總的作用,也是就是說base=cells[0]+cells[1]……+base

cellbase則用來表示加鎖置位,0表示無鎖,1表示加鎖。

注意:
此處cellsbusy所說的鎖並不是真正的對象鎖,而是底層用cas來模擬加鎖。

cas模擬加鎖:

當佔用某資源需要模擬加鎖時,cas會將某處標誌位的初始態變爲加鎖態(如將0改爲1)。此時當出現競爭時,其他線程同樣會嘗試cas操作,但均以失敗告終,所以會不停嘗試cas,起到了加鎖的功效。

@sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }
 
        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long valueOffset;
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }

 

在Cell類源碼中我們能看出,cell底層也是採用cas來做累加計數。

三、僞共享
我們都知道cpu內存模型

因爲 CPU 與內存的速度差異很大,需要靠預讀數據至緩存來提升效率。而緩存以緩存行爲單位,每個緩存行對應着一塊內存,一般是64 byte (8個 long )

緩存的加入會造成數據副本的產生,即同一份數據會緩存在不同核心的緩存行中。

CPU 要保證數據的一致性,如果某個 CPU 核心更改了數據,其它 CPU 核心對應的整個緩存行必須失效

也就是說,在同一緩存行中的緩存的內容時時刻刻都是相同的,這樣就違背了我們cells的初衷。

 

 

 

如圖,

因爲cells是數組類型,導致他們在內存中始終處於連續存儲狀態。

當任何一個線程改變cell中的值時,另一個線程中的緩存必然會失效(緩存是以行爲單位進行更新),這樣繁雜的操作使得效率大大降低。

問題解決
@ sun . misc . Contended 用來解決這個問題,

它的原理是在使用此註解的對象或字段的前後各增加128字節大小的 padding 從而讓 CPU 將對象預讀至緩存時佔用不同的緩存行,這樣,不會造成對方緩存行的失效。

也就是說,增加無用的內存空間是cells擴容,從而在緩存中,每一個cell能佔據一個緩存行,也就解決了失效的問題。

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