LongAdder和AtomicLong相比的優勢分析

熟悉AtomicLong的都知道,它通過CAS操作解決原子性問題,但是這個類的設計也還有一些確定,在高併發下大量線程會同時更新同一個原子變量,但由於同時只可能有一個線程更新成功,這就會造成大量線程競爭失敗,失敗後則會通過無限的循環不斷的進行自旋重新嘗試CAS操作,這樣會導致大量浪費CPU的資源,所以jdk1.8之後新增了一個LongAdder來解決這個問題。


它採樣的主要思想是分而治之,既然多個線程競爭一個資源,如果把一個資源分解為多個小資源,是不是就會解決性能的問題。


今天帶著這個問題分析一下LongAdder的源碼。

這個類繼承自Striped64,內部有三個變量,分別是cells、base、cellsBusy,且三個變量都用volatile進行修飾,保證內存的可見性。

LongAdder和AtomicLong相比的優勢分析

cells是一個數組,長度為2的冪次方,base是個最基礎的值,通過cas更新,其實最後LongAdder的實際值是base的值和cells的值之和。cellsBusy通過cas進行自旋,在擴容或者創建cells的時候。


下面先看一段代碼。

LongAdder和AtomicLong相比的優勢分析

代碼很簡單,創建一個對象,然後加1,最後打印。


LongAdder和AtomicLong相比的優勢分析

increment其實最終調用的還是add方法,只是入參為1.

先看86行的代碼,第一次進來cells肯定為空,這點可以在Striped64裡得到驗證。

如果是單線程中使用LongAdder,則每次在調用方法casBase進行CAS操作時,是不存在競爭的,換言之,cells也會一值為空,那用此類也沒有意義了。


多線程情況下,執行方法casBase進行cas操作失敗時,則執行代碼88行,在第一次執行時,as為空則會執行91行的代碼,詳情如下圖:

<code>/**
* Same as longAccumulate, but injecting long/double conversions
* in too many places to sensibly merge with long version, given
* the low-overhead requirements of this class. So must instead be
* maintained by copy/paste/adapt.
*/
final void doubleAccumulate(double x, DoubleBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(Double.doubleToRawLongBits(x));
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}

collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value,
((fn == null) ?
Double.doubleToRawLongBits
(Double.longBitsToDouble(v) + x) :
Double.doubleToRawLongBits
(fn.applyAsDouble
(Double.longBitsToDouble(v), x)))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(Double.doubleToRawLongBits(x));
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base,
((fn == null) ?
Double.doubleToRawLongBits

(Double.longBitsToDouble(v) + x) :
Double.doubleToRawLongBits
(fn.applyAsDouble
(Double.longBitsToDouble(v), x)))))
break; // Fall back on using base
}
}/<code>


這段代碼主要完成了cells的初始化和擴容。


假設有多個線程同時執行到這個方法時,當第一線程執行到這個方法時,cells為空,則會執行61行的判斷,即【else if (cellsBusy == 0 && cells == as && casCellsBusy())】

執行casCellsBusy成功,即把cellsBusy通過CAS操作修改為1,則開始進行cells的初始化,數組長度為2,最後把cellsBusy重新設置為0。初始化成功後,當前線程則退出for循環。即使多個線程同時執行到這裡,但是cas只會成功一次。則第二個線程則會走到上述的76行邏輯,嘗試對base進行CAS操作,如果執行成功,則同樣退出for循環。


有一個重要的變量h,默認值為0,第一個線程執行到上述第4行時,會進行重置,在ThreadLocalRandom.current()裡執行,這裡會強制進行初始化,設置當前線程的值。代碼如下:

LongAdder和AtomicLong相比的優勢分析

LongAdder和AtomicLong相比的優勢分析

注意208行的代碼,跟進去之後也進行CAS操作。

LongAdder和AtomicLong相比的優勢分析


剛提到h這個變量主要是用來計算在cells的落點,下面看下如果兩個線程在同時一刻競爭到同一落點時的操作,繼續跟進

LongAdder和AtomicLong相比的優勢分析

假設兩個線程都執行到了313行的代碼,但是在執行316行的代碼時,肯定只會有一個執行成功,因為casCellsBusy進行了CAS操作。則其中一個線程會完成設置,並把cellsBusy重新設置為0.則另外一個線程則會繼續自旋,同樣再次執行到313行代碼時,發現此處不為空時,則會走到下面的代碼:

LongAdder和AtomicLong相比的優勢分析

這裡嘗試在a這個下標位置嘗試CAS操作,如成功,則退出for循環。

如果失敗則繼續自旋。


LongAdder和AtomicLong相比的優勢分析

這裡定義了cells的長度不會超過cpu的個數,也就是說LongAdder的並行度為cpu的個數。

LongAdder和AtomicLong相比的優勢分析

這裡定義了cells的擴容條件及擴容長度,2的倍數。

在高併發下,因為有CAS操作,所以總會有一次cells的初始化和擴容,當計算下標發現此處的值不為空,則繼續進行CAS操作,失敗則自旋,總會有一次成功。


如何獲取LongAdder的值,通過sum、longValue、intValue其實都可以,後兩種方法本質也是調用的sum方法。


看下代碼:

LongAdder和AtomicLong相比的優勢分析

這裡其實是base+cells的值然後進行累加。


看到這裡,是不是覺著LongAdder比AtomicLong要牛逼多了。

最後再看下Cell

LongAdder和AtomicLong相比的優勢分析


<code>@sun.misc.Contended,這個用來解決偽共享。Cell內部的value也是內存可見的。/<code> 


今天的內容你看懂了麼?LongAdder的好處多多,可以用起來了,哈哈。


分享到:


相關文章: