一、原子累加器
我們都知道,原子整型可以在線程安全的前提下做到累加功能,而今天介紹的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能佔據一個緩存行,也就解決了失效的問題。