本博客系列是學習並發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。
原子類型累加器是JDK1.8引進的並發新技術,它可以看做AtomicLong和AtomicDouble的部分加強類型。
原子類型累加器有如下四種:
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
由於上面四種累加器的原理類似,下面以LongAdder為列來介紹累加器的使用。以下內存是轉載內容,原文請見此博客。
LongAdder簡介
JDK1.8時,java.util.concurrent.atomic
包中提供了一個新的原子類:LongAdder
。
根據Oracle官方文檔的介紹,LongAdder在高並發的場景下會比它的前輩————AtomicLong 具有更好的性能,代價是消耗更多的內存空間:
那么,問題來了:
為什么要引入LongAdder? AtomicLong在高並發的場景下有什么問題嗎? 如果低並發環境下,LongAdder和AtomicLong性能差不多,那LongAdder是否就可以替代AtomicLong了?
為什么要引入LongAdder?
我們知道,AtomicLong是利用了底層的CAS操作來提供並發性的,比如addAndGet方法:
上述方法調用了Unsafe類的getAndAddLong方法,該方法是個native方法,它的邏輯是采用自旋的方式不斷更新目標值,直到更新成功。
在並發量較低的環境下,線程沖突的概率比較小,自旋的次數不會很多。但是,高並發環境下,N個線程同時進行自旋操作,會出現大量失敗並不斷自旋的情況,此時AtomicLong的自旋會成為瓶頸。
這就是LongAdder引入的初衷——解決高並發環境下AtomicLong的自旋瓶頸問題。
LongAdder快在哪里?
既然說到LongAdder可以顯著提升高並發環境下的性能,那么它是如何做到的?這里先簡單的說下LongAdder的思路,第二部分會詳述LongAdder的原理。
我們知道,AtomicLong中有個內部變量value保存着實際的long值,所有的操作都是針對該變量進行。也就是說,高並發環境下,value變量其實是一個熱點,也就是N個線程競爭一個熱點。
LongAdder的基本思路就是分散熱點,將value值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,沖突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。
這種做法有沒有似曾相識的感覺?沒錯,ConcurrentHashMap中的“分段鎖”其實就是類似的思路。
LongAdder能否替代AtomicLong?
回答這個問題之前,我們先來看下LongAdder提供的API:
可以看到,LongAdder提供的API和AtomicLong比較接近,兩者都能以原子的方式對long型變量進行增減。
但是AtomicLong提供的功能其實更豐富,尤其是addAndGet、decrementAndGet、compareAndSet這些方法。
addAndGet、decrementAndGet除了單純的做自增自減外,還可以立即獲取增減后的值,而LongAdder則需要做同步控制才能精確獲取增減后的值。如果業務需求需要精確的控制計數,做計數比較,AtomicLong也更合適。
另外,從空間方面考慮,LongAdder其實是一種“空間換時間”的思想,從這一點來講AtomicLong更適合。當然,如果你一定要跟我杠現代主機的內存對於這點消耗根本不算什么,那我也辦法。
總之,低並發、一般的業務場景下AtomicLong是足夠了。如果並發量很多,存在大量寫多讀少的情況,那LongAdder可能更合適。適合的才是最好的,如果真出現了需要考慮到底用AtomicLong好還是LongAdder的業務場景,那么這樣的討論是沒有意義的,因為這種情況下要么進行性能測試,以准確評估在當前業務場景下兩者的性能,要么換個思路尋求其它解決方案。
最后,給出國外一位博主對LongAdder和AtomicLong的性能評測,以供參考:http://blog.palominolabs.com/...
LongAdder原理
之前說了,AtomicLong是多個線程針對單個熱點值value進行原子操作。而LongAdder是每個線程擁有自己的槽,各個線程一般只對自己槽中的那個值進行CAS操作。
比如有三個ThreadA、ThreadB、ThreadC,每個線程對value增加10。
對於AtomicLong,最終結果的計算始終是下面這個形式:
value = 10 + 10 + 10
但是對於LongAdder來說,內部有一個base
變量,一個Cell[]
數組。
base
變量:非競態條件下,直接累加到該變量上
Cell[]
數組:競態條件下,累加個各個線程自己的槽Cell[i]
中
最終結果的計算是下面這個形式:
LongAdder的內部結構
LongAdder只有一個空構造器,其本身也沒有什么特殊的地方,所有復雜的邏輯都在它的父類Striped64中。
來看下Striped64的內部結構,這個類實現一些核心操作,處理64位數據。
Striped64只有一個空構造器,初始化時,通過Unsafe獲取到類字段的偏移量,以便后續CAS操作:
上面有個比較特殊的字段是threadLocalRandomProbe
,可以把它看成是線程的hash值。這個后面我們會講到。
定義了一個內部Cell類,這就是我們之前所說的槽,每個Cell對象存有一個value值,可以通過Unsafe來CAS操作它的值:
其它的字段:
可以看到Cell[]就是之前提到的槽數組,base就是非並發條件下的基數累計值。
LongAdder的核心方法
還是通過例子來看:
假設現在有一個LongAdder對象la,四個線程A、B、C、D同時對la進行累加操作。
LongAdder la = new LongAdder();
la.add(10);
①ThreadA調用add方法(假設此時沒有並發):
初始時Cell[]為null,base為0。所以ThreadA會調用casBase方法(定義在Striped64中),因為沒有並發,CAS操作成功將base變為10:
可以看到,如果線程A、B、C、D線性執行,那casBase永遠不會失敗,也就永遠不會進入到base方法的if塊中,所有的值都會累積到base中。
那么,如果任意線程有並發沖突,導致caseBase失敗呢?
失敗就會進入if方法體:
這個方法體會先再次判斷Cell[]槽數組有沒初始化過,如果初始化過了,以后所有的CAS操作都只針對槽中的Cell;否則,進入longAccumulate方法。
整個add方法的邏輯如下圖:
可以看到,只有從未出現過並發沖突的時候,base基數才會使用到,一旦出現了並發沖突,之后所有的操作都只針對
Cell[]
數組中的單元Cell。
如果Cell[]
數組未初始化,會調用父類的longAccumelate
去初始化Cell[]
,如果Cell[]
已經初始化但是沖突發生在Cell
單元內,則也調用父類的longAccumelate
,此時可能就需要對Cell[]
擴容了。
這也是LongAdder設計的精妙之處:盡量減少熱點沖突,不到最后萬不得已,盡量將CAS操作延遲。
Striped64的核心方法
我們來看下Striped64的核心方法longAccumulate到底做了什么:
上述代碼首先給當前線程分配一個hash值,然后進入一個自旋,這個自旋分為三個分支:
- CASE1:Cell[]數組已經初始化
- CASE2:Cell[]數組未初始化
- CASE3:Cell[]數組正在初始化中
CASE2:Cell[]數組未初始化
我們之前討論了,初始時Cell[]數組還沒有初始化,所以會進入分支②:
首先會將cellsBusy置為1-加鎖狀態
然后,初始化Cell[]數組(初始大小為2),根據當前線程的hash值計算映射的索引,並創建對應的Cell對象,Cell單元中的初始值x就是本次要累加的值。
CASE3:Cell[]數組正在初始化中
如果在初始化過程中,另一個線程ThreadB也進入了longAccumulate方法,就會進入分支③:
可以看到,分支③直接操作base基數,將值累加到base上。
CASE1:Cell[]數組已經初始化
如果初始化完成后,其它線程也進入了longAccumulate方法,就會進入分支①:
整個longAccumulate的流程圖如下:
LongAdder的sum方法
最后,我們來看下LongAdder的sum方法:
sum求和的公式就是我們開頭說的:
需要注意的是,這個方法只能得到某個時刻的近似值,這也就是LongAdder並不能完全替代LongAtomic的原因之一。
PS: 由於計算總和時沒有對Cell數組進行加鎖,所以在累加過程中可能有其他線程對Cell中的值進行了修改,也有可能對數組進行了擴容,所以sum返回的值並不是非常精確的,其返回值並不是一個調用sum方法時的原子快照值。
LongAdder的其它兄弟
JDK1.8時,java.util.concurrent.atomic
包中,除了新引入LongAdder外,還有引入了它的三個兄弟類:LongAccumulator、DoubleAdder、DoubleAccumulator
LongAccumulator
LongAccumulator是LongAdder的增強版。LongAdder只能針對數值的進行加減運算,而LongAccumulator提供了自定義的函數操作。其構造函數如下:
通過LongBinaryOperator,可以自定義對入參的任意操作,並返回結果(LongBinaryOperator接收2個long作為參數,並返回1個long)
LongAccumulator內部原理和LongAdder幾乎完全一樣,都是利用了父類Striped64的longAccumulate方法。這里就不再贅述了,讀者可以自己閱讀源碼。
DoubleAdder和DoubleAccumulator
從名字也可以看出,DoubleAdder和DoubleAccumulator用於操作double原始類型。
與LongAdder的唯一區別就是,其內部會通過一些方法,將原始的double類型,轉換為long類型,其余和LongAdder完全一樣: