Java多線程進階(十七)—— J.U.C之atomic框架:LongAdder

2.jpg

本文首發於一世流雲的專欄: https://segmentfault.com/blog...

1、LongAdder簡介

JDK1.8時,java.util.concurrent.atomic包中提供了一個新的原子類:LongAdder
根據Oracle官方文檔的介紹,LongAdder在高併發的場景下會比它的前輩————AtomicLong 具備更好的性能,代價是消耗更多的內存空間:
clipboard.pngjava

那麼,問題來了:segmentfault

爲何要引入LongAdderAtomicLong在高併發的場景下有什麼問題嗎? 若是低併發環境下,LongAdderAtomicLong性能差很少,那LongAdder是否就能夠替代AtomicLong了?

爲何要引入LongAdder?

咱們知道,AtomicLong是利用了底層的CAS操做來提供併發性的,好比addAndGet方法:數組

clipboard.png

上述方法調用了Unsafe類的getAndAddLong方法,該方法是個native方法,它的邏輯是採用自旋的方式不斷更新目標值,直到更新成功。併發

在併發量較低的環境下,線程衝突的機率比較小,自旋的次數不會不少。可是,高併發環境下,N個線程同時進行自旋操做,會出現大量失敗並不斷自旋的狀況,此時AtomicLong的自旋會成爲瓶頸。dom

這就是LongAdder引入的初衷——解決高併發環境下AtomicLong的自旋瓶頸問題。函數

LongAdder快在哪裏?

既然說到LongAdder能夠顯著提高高併發環境下的性能,那麼它是如何作到的?這裏先簡單的說下LongAdder的思路,第二部分會詳述LongAdder的原理。高併發

咱們知道,AtomicLong中有個內部變量value保存着實際的long值,全部的操做都是針對該變量進行。也就是說,高併發環境下,value變量實際上是一個熱點,也就是N個線程競爭一個熱點。性能

LongAdder的基本思路就是分散熱點,將value值分散到一個數組中,不一樣線程會命中到數組的不一樣槽中,各個線程只對本身槽中的那個值進行CAS操做,這樣熱點就被分散了,衝突的機率就小不少。若是要獲取真正的long值,只要將各個槽中的變量值累加返回。測試

這種作法有沒有似曾相識的感受?沒錯,ConcurrentHashMap中的「分段鎖」其實就是相似的思路。atom

LongAdder可否替代AtomicLong?

回答這個問題以前,咱們先來看下LongAdder提供的API:
clipboard.png

能夠看到,LongAdder提供的API和AtomicLong比較接近,二者都能以原子的方式對long型變量進行增減。

可是AtomicLong提供的功能其實更豐富,尤爲是addAndGetdecrementAndGetcompareAndSet這些方法。

addAndGetdecrementAndGet除了單純的作自增自減外,還能夠當即獲取增減後的值,而LongAdder則須要作同步控制才能精確獲取增減後的值。若是業務需求須要精確的控制計數,作計數比較,AtomicLong也更合適。

另外,從空間方面考慮,LongAdder實際上是一種「空間換時間」的思想,從這一點來說AtomicLong更適合。固然,若是你必定要跟我槓現代主機的內存對於這點消耗根本不算什麼,那我也辦法。

總之,低併發、通常的業務場景下AtomicLong是足夠了。若是併發量不少,存在大量寫多讀少的狀況,那LongAdder可能更合適。適合的纔是最好的,若是真出現了須要考慮到底用AtomicLong好仍是LongAdder的業務場景,那麼這樣的討論是沒有意義的,由於這種狀況下要麼進行性能測試,以準確評估在當前業務場景下二者的性能,要麼換個思路尋求其它解決方案。

最後,給出國外一位博主對LongAdder和AtomicLong的性能評測,以供參考:http://blog.palominolabs.com/...

2、LongAdder原理

以前說了,AtomicLong是多個線程針對單個熱點值value進行原子操做。而LongAdder是每一個線程擁有本身的槽,各個線程通常只對本身槽中的那個值進行CAS操做。

好比有三個ThreadA、ThreadB、ThreadC,每一個線程對value增長10。

對於AtomicLong,最終結果的計算始終是下面這個形式:
$$ value = 10 + 10 + 10 = 30 $$

可是對於LongAdder來講,內部有一個base變量,一個Cell[]數組。
base變量:非競態條件下,直接累加到該變量上
Cell[]數組:競態條件下,累加個各個線程本身的槽Cell[i]
最終結果的計算是下面這個形式:
$$ value = base + \sum_{i=0}^nCell[i] $$

LongAdder的內部結構

LongAdder只有一個空構造器,其自己也沒有什麼特殊的地方,全部複雜的邏輯都在它的父類Striped64中。
clipboard.png

來看下Striped64的內部結構,這個類實現一些核心操做,處理64位數據。
Striped64只有一個空構造器,初始化時,經過Unsafe獲取到類字段的偏移量,以便後續CAS操做:
clipboard.png

上面有個比較特殊的字段是threadLocalRandomProbe,能夠把它當作是線程的hash值。這個後面咱們會講到。

定義了一個內部Cell類,這就是咱們以前所說的槽,每一個Cell對象存有一個value值,能夠經過Unsafe來CAS操做它的值:
clipboard.png

其它的字段:
能夠看到Cell[]就是以前提到的槽數組,base就是非併發條件下的基數累計值。
clipboard.png

LongAdder的核心方法

仍是經過例子來看:
假設如今有一個LongAdder對象la,四個線程A、B、C、D同時對la進行累加操做。

LongAdder la = new LongAdder();
la.add(10);

ThreadA調用add方法(假設此時沒有併發):
clipboard.png

初始時Cell[]爲null,base爲0。因此ThreadA會調用casBase方法(定義在Striped64中),由於沒有併發,CAS操做成功將base變爲10:
clipboard.png

能夠看到,若是線程A、B、C、D線性執行,那casBase永遠不會失敗,也就永遠不會進入到base方法的if塊中,全部的值都會累積到base中。
那麼,若是任意線程有併發衝突,致使caseBase失敗呢?

失敗就會進入if方法體:
clipboard.png

這個方法體會先再次判斷Cell[]槽數組有沒初始化過,若是初始化過了,之後全部的CAS操做都只針對槽中的Cell;不然,進入longAccumulate方法。

整個add方法的邏輯以下圖:
clipboard.png

能夠看到,只有從未出現過併發衝突的時候,base基數纔會使用到,一旦出現了併發衝突,以後全部的操做都只針對 Cell[]數組中的單元Cell。
若是 Cell[]數組未初始化,會調用父類的 longAccumelate去初始化 Cell[],若是 Cell[]已經初始化可是衝突發生在 Cell單元內,則也調用父類的 longAccumelate,此時可能就須要對 Cell[]擴容了。

這也是LongAdder設計的精妙之處:儘可能減小熱點衝突,不到最後萬不得已,儘可能將CAS操做延遲。

Striped64的核心方法

咱們來看下Striped64的核心方法longAccumulate到底作了什麼:
clipboard.png

上述代碼首先給當前線程分配一個hash值,而後進入一個自旋,這個自旋分爲三個分支:

  • CASE1:Cell[]數組已經初始化
  • CASE2:Cell[]數組未初始化
  • CASE3:Cell[]數組正在初始化中

CASE2:Cell[]數組未初始化

咱們以前討論了,初始時Cell[]數組尚未初始化,因此會進入分支②:
clipboard.png

首先會將cellsBusy置爲1-加鎖狀態
clipboard.png

而後,初始化Cell[]數組(初始大小爲2),根據當前線程的hash值計算映射的索引,並建立對應的Cell對象,Cell單元中的初始值x就是本次要累加的值。

CASE3:Cell[]數組正在初始化中

若是在初始化過程當中,另外一個線程ThreadB也進入了longAccumulate方法,就會進入分支③:
clipboard.png

能夠看到,分支③直接操做base基數,將值累加到base上。

CASE1:Cell[]數組已經初始化

若是初始化完成後,其它線程也進入了longAccumulate方法,就會進入分支①:
clipboard.png

整個longAccumulate的流程圖以下:
clipboard.png

LongAdder的sum方法

最後,咱們來看下LongAddersum方法:
clipboard.png

sum求和的公式就是咱們開頭說的:
$$ value = base + \sum_{i=0}^nCell[i] $$

須要注意的是,這個方法只能獲得某個時刻的近似值,這也就是LongAdder並不能徹底替代LongAtomic的緣由之一。

3、LongAdder的其它兄弟

JDK1.8時,java.util.concurrent.atomic包中,除了新引入LongAdder外,還有引入了它的三個兄弟類:LongAccumulatorDoubleAdderDoubleAccumulator

clipboard.png

LongAccumulator

LongAccumulatorLongAdder的加強版。LongAdder只能針對數值的進行加減運算,而LongAccumulator提供了自定義的函數操做。其構造函數以下:
clipboard.png

經過LongBinaryOperator,能夠自定義對入參的任意操做,並返回結果(LongBinaryOperator接收2個long做爲參數,並返回1個long)

LongAccumulator內部原理和LongAdder幾乎徹底同樣,都是利用了父類Striped64longAccumulate方法。這裏就再也不贅述了,讀者能夠本身閱讀源碼。

DoubleAdder和DoubleAccumulator

從名字也能夠看出,DoubleAdderDoubleAccumulator用於操做double原始類型。

LongAdder的惟一區別就是,其內部會經過一些方法,將原始的double類型,轉換爲long類型,其他和LongAdder徹底同樣:
clipboard.png

相關文章
相關標籤/搜索