面試必備:Java JUC Atomic LongAdder 詳解

基於OpenJDK 12

閱讀本文前,推薦先閱讀以下兩篇文章,以便能更好的對比理解:

[譯]Java Concurrent Atomic Package詳解

面試必備:Java JUC AtomicLong 實現解析


LongAdder是JDK 1.8 新增的原子類,基於Striped64實現。從官方文檔看,LongAdder在高併發的場景下會比AtomicLong 具有更好的性能,代價是消耗更多的內存空間:

This class is usually preferable to AtomicLong when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization control. Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.

那麼LongAdder是怎麼實現的?

先看一下LongAdder的類圖:

640?wx_fmt=png

基類Number,Number是一個抽象類其中沒有任何邏輯,該類是byte、double、float、int、long、short的基類。


一、Striped64

1、設計思想

該部分翻譯自Striped64源碼註釋,可以略過,概括起來就是:

分散熱點,將value值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,衝突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。


從Striped64類註釋可以看到:

Striped64是package內使用的,對於在64位元素上動態分片提供統一實現(感覺有點像:AbstractQueuedSynchronizer)
Striped64繼承了Number類,這也就是說具體實現的子類也必須實現相關的內容

該類維護一個原子更新變量的延遲初始化表,以及一個額外的“base”字段。表的大小是2的冪。索引使用掩碼下的每個線程的hash code。這個類中的幾乎所有聲明都是package私有的,由子類直接訪問。

該類維護一個原子更新變量的延遲初始化表,以及一個額外的“base”字段。表的大小是2的冪。索引使用掩碼下的每個線程的hash code。這個類中的幾乎所有聲明都是package私有的,由子類直接訪問。

表格內的元素是Cell類,Cell類是一個爲了減少緩存爭用而填充的AtomicLong的變種。填充對於大多數原子來說是多餘的,因爲它們通常不規則地分散在內存中,因此彼此之間不會有太多的干擾。但是,駐留在數組中的原子對象往往是彼此相鄰的,因此在沒有這種預防措施的情況下,最常見的情況是共享高速緩存線(這對性能有很大的負面影響)。

在某種程度上,因爲Cell類相對較大,只有他們真正被需要的時候,我們才創建。如果沒有競爭,那麼所有的更新操作將對base字段實現。當發生第一次爭用(也就是說如果第一次對base字段的CAS操作失敗),初始化爲大小是2的表格。當進一步的爭用發生的時候,表的大小會加倍,直到達到等於大於cpu的數量。表在未使用之前一直爲null。

利用一個自旋鎖(cellsBusy)來初始化和調整表的大小,以及用新Cells填充slots。這個地方沒有必要使用阻塞鎖,如果鎖不可達,線程可以嘗試其他的slots,或者嘗試base字段。在這些重試期間,競爭加劇,但是降低了局部性,這仍然比阻塞鎖來得好。

通過ThreadLocalRandom維護的Thread.probe字段用作每個線程的哈希碼。我們讓它們保持未初始化(爲零)(如果它們以這種方式出現),直到它們在插槽0競爭。出現競爭後初始化爲通常不會和其他的的值衝突的值,比如線程的哈希碼。在執行更新時發生CAS操作失敗意味着出現了爭用或者表碰撞,或兩者都有。在發生衝突時,如果表的大小小於容量,那麼它的大小將加倍,除非其他線程持有鎖。如果哈希後的slot爲空,並且鎖可用,則創建一個新單元格。如果存在了那麼會進行CAS嘗試。通過雙重哈希進行重試,利用一個輔助哈希(Marsaglia XorShift隨機數算法)來嘗試尋找一個空閒的slot。

表的大小是有上限的,因爲當線程多於CPU時,假設每個線程都綁定到一個CPU,就會有一個完美的散列函數將線程映射到插槽,從而消除衝突。當我們達到容量時,我們通過隨機改變衝突線程的哈希代碼來搜索這個映射。因爲搜索是隨機的,衝突只有通過CAS失敗才知道,收斂可能會很慢,而且因爲線程通常不會永遠綁定到CPU,所以根本不會發生。然而,儘管有這些限制,在這些情況下觀察到的競爭率通常很低。

Cell可能會出現不可用的情況,包括進行哈希的線程終止,或者由於table擴容導致線程哈希不正確。我們不嘗試檢測或刪除這樣的單元格,假設對於長時間運行的實例,爭用會再次發生,因此最終將再次需要這些單元格;而對於短時間運行的實例,花費時間去銷燬又沒有什麼必要。


2、Cell類

Atomiclong的變體,僅支持原始訪問和CAS。

Cell類被註解@jdk.internal.vm.annotation.Contended修飾。
Contended的作用(詳細信息參見:JEP 142):
Define a way to specify that one or more fields in an object are likely to be highly contended across processor cores so that the VM can arrange for them not to share cache lines with other fields, or other objects, that are likely to be independently accessed.

3、示意圖

640?wx_fmt=png

4、具體實現

Striped64的核心方法是longAccumulate、doubleAccumulate,兩者類似,下面主要看一下longAccumulate,對於這種代碼,個人建議是理解思路即可,畢竟咱們又不是過來修改JDK的,如果真的要修改了或者有類似的需求了,再回來細看即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//x  元素
//fn 更新函數,如果是add可以爲null(這個約定避免了longadder中定義額外的變量或者函數)
//wasUncontended 如果CAS在調用之前失敗了,這個值爲false
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
//獲取當前線程的probe值,如果爲0,則需要初始化該線程的probe值
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
done: for (;;) {
Cell[] cs; Cell c; int n; long v;
//Cells不爲空,進行操作
if ((cs = cells) != null && (n = cs.length) > 0) {
//通過(hashCode & (length - 1))這種算法來實現取模 有種看到HashMap代碼的感覺
//如果當前位置爲null說明需要初始化
if ((c = cs[(n - 1) & h]) == null) {
//判斷鎖狀態
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
//再次判斷鎖狀態,同時獲取鎖
if (cellsBusy == 0 && casCellsBusy()) {
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;
//創建成功跳出
break done;
}
} finally {
//釋放鎖
cellsBusy = 0;
}
continue; // Slot is now non-empty
}
}
collide = false;
}
//運行到此說明cell的對應位置上已經有相應的Cell了,
//不需要初始化了
//CAS操作已經失敗了,出現了競爭
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//這裏嘗試將x值加到a的value上
else if (c.cas(v = c.value,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
//如果嘗試成功,跳出循環,方法退出
break;
//cell數組最大爲cpu的數量,
//cells != as表明cells數組已經被更新了
//標記爲最大狀態或者說是過期狀態
else if (n >= NCPU || cells != cs)
collide = false; // At max size or stale
else if (!collide)
collide = true;
//擴容 當前容量 * 2
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == cs) // Expand table unless stale
cells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
//嘗試獲取鎖之後擴大Cells
else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
try { // Initialize table
if (cells == cs) {
//初始化cell表,初始容量爲2。
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
break done;
}
} finally {
//釋放cellsBusy鎖
cellsBusy = 0;
}
}
//如果創建cell表由於競爭導致失敗,嘗試將x累加到base上
// Fall back on using base
else if (casBase(v = base,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break done;
}
}
/**
* CASes the cellsBusy field from 0 to 1 to acquire lock.
*/
final boolean casCellsBusy() {
return CELLSBUSY.compareAndSet(this, 0, 1);
}
/**
* CASes the base field.
*/
final boolean casBase(long cmp, long val) {
return BASE.compareAndSet(this, cmp, val);
}

這一段的核心是這樣的:

  • longAccumulate會根據當前線程來計算一個哈希值,然後根據(hashCode & (length - 1))取模,以定位到該線程被分散到的Cell數組中的位置

  • 如果Cell數組還沒有被創建,那麼就去獲取cellBusy這個鎖(相當於鎖,但是更爲輕量級),如果獲取成功,則初始化Cell數組,初始容量爲2,初始化完成之後將x包裝成一個Cell,哈希計算之後分散到相應的index上。如果獲取cellBusy失敗,那麼會試圖將x累計到base上,更新失敗會重新嘗試直到成功。

  • 如果Cell數組已經被初始化過了,那麼就根據線程的哈希值分散到一個Cell數組元素上,獲取這個位置上的Cell並且賦值給變量a,如果a爲null,說明該位置還沒有被初始化,那麼就初始化,當然在初始化之前需要競爭cellBusy變量。

  • 如果Cell數組的大小已經最大了(大於等於CPU的數量),那麼就需要重新計算哈希,來重新分散當前線程到另外一個Cell位置上再走一遍該方法的邏輯,否則就需要對Cell數組進行擴容,然後將原來的計數內容遷移過去。由於Cell裏面保存的是計數值,所以擴容後沒有必要做其他處理,直接根據index將舊的Cell數組內容複製到新的Cell數組中。

二、LongAdder

LongAdder的基本思路就是分散熱點,將value值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,衝突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。

1、說明

保持一個或者多個變量,初始值設置爲零用於求和。當更新出現多個線程競爭時,變量集合動態增長以減少爭用。最後當需要求和的時候或者說需要這個Long型的值時,可以通過把當前這些變量求和,合併後得出最終的和。

LongAdder在一些高併發場景下表現要比AtomicLong好,比如多個線程同時更新一個求和的變量,比如統計集合的數量,但是不能用於細粒度同步控制,換句話說這個是可能有誤差的(因爲更新與讀取是並行的)。在低併發場景場景下LongAdder和AtomicLong的性能表現沒什麼差別,但是當高併發競爭的時候,這個類將具備更好的吞吐性能,但是相應的也會耗費相當的空間。

LongAdder繼承了Number抽象類,但是並沒有實現一些方法例如: equals、hashCode、compareTo,因爲LongAdder實例的預期用途是進行一些比較頻繁的變化,所以也不適合作爲集合的key。

2、具體實現

看一下LongAdder有哪些方法:

640?wx_fmt=png

下面主要解析LongAdder increment、sum方法,先看一下源碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* Equivalent to {@code add(1)}.
*/
public void increment() {
add(1L);
}

/**
* Adds the given value.
*
* @param x the value to add
*/
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
//到了這裏 表明cs不爲null or 線程有併發衝突,導致caseBase失敗
boolean uncontended = true;
if (cs == null || // cells 爲null
(m = cs.length - 1) < 0 || // cells 不爲null 但只有一個元素
(c = cs[getProbe() & m]) == null || //哈希取模 對應位置元素爲null
!(uncontended = c.cas(v = c.value, v + x))) //cas 替換失敗(併發競爭)
longAccumulate(x, null, uncontended);
}
}
/**
* CASes the base field (Striped64類中的方法)
*/
final boolean casBase(long cmp, long val) {
return BASE.compareAndSet(this, cmp, val);
}

//當在sum的過程中,有可能別的線程正在操作cells(因爲沒有加鎖)
//sum取的值,不一定準確
public long sum() {
Cell[] cs = cells;
long sum = base;
if (cs != null) {
for (Cell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}

4、LongAdder vs AtomicLong 

Java 8 Performance Improvements: LongAdder vs AtomicLong

三、Reference

https://github.com/jiankunking/openjdk12/blob/master/src/java.base/share/classes/java/util/concurrent/atomic/LongAdder.java

https://github.com/jiankunking/openjdk12/blob/master/src/java.base/share/classes/jdk/internal/vm/annotation/Contended.java

https://www.jianshu.com/p/9a7de5644dd4

http://openjdk.java.net/jeps/142

http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html


< END >

喜歡就點個在看 or 轉發個朋友圈唄


640?wx_fmt=jpeg

衣舞晨風



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