比AtomicLong更高效的併發計數類LongAdder
最近在看(輕量級的流量控制、熔斷降級 Java 庫)原始碼的時候,看到在統計數量的時候使用了LongAdder。這個LongAdder是jdk1.8新增的,出自Doug Lea之手,偉大的Java併發大師的鼻祖。在沒有接觸到LongAdder之前,AtomicLong這個類在併發計數上無論效能還是準確性已經做得極好了。在阿里流控框架中使用這樣一個LongAdder類,必然有其過人之處。
回顧AtomicLong
AtomicLong是透過CAS(即Compare And Swap)原理來完成原子遞增遞減操作,在併發情況下不會出現執行緒不安全結果。AtomicLong中的value是使用volatile修飾,併發下各個執行緒對value最新值均可見。我們以incrementAndGet()方法來深入。
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
1
2
3
這裡是呼叫了unsafe的方法
public final long getAndAddLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
return var6;
}
1
2
3
4
5
6
7
8
方法中this.compareAndSwapLong()有4個引數,var1是需要修改的類物件,var2是需要修改的欄位的記憶體地址,var6是修改前欄位的值,var6+var4是修改後欄位的值。compareAndSwapLong只有該欄位實際值和var6值相當的時候,才可以成功設定其為var6+var4。
再繼續往深一層去看
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
1
這裡Unsafe.compareAndSwapLong是native方法,底層透過JNI(Java Native Interface)來完成呼叫,實際就是藉助C來呼叫CPU指令來完成。
實現中使用了do-while迴圈,如果CAS失敗,則會繼續重試,直到成功為止。併發特別高的時候,雖然這裡可能會有很多次迴圈,但是還是可以保證執行緒安全的。不過如果自旋CAS操作長時間不成功,競爭較大,會帶CPU帶來極大的開銷,佔用更多的執行資源,可能會影響其他主業務的計算等。
LongAdder怎麼最佳化AtomicLong
Doug Lea在jdk1.5的時候就針對HashMap進行了執行緒安全和併發效能的最佳化,推出了分段鎖實現的ConcurrentHashMap。一般Java面試,基本上離不開ConcurrentHashMap這個網紅問題。另外在ForkJoinPool中,Doug Lea在其工作竊取演算法上對WorkQueue使用了細粒度鎖來較少併發的競爭,更多細節可參考我的原創文章ForkJoin使用和原理剖析。如果已經對ConcurrentHashMap有了較為深刻的理解,那麼現在來看LongAdder的實現就會相對簡單了。
來看LongAdder的increase()方法實現,
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
//第一個if進行了兩個判斷,(1)如果cells不為空,則直接進入第二個if語句中。(2)同樣會先使用cas指令來嘗試add,如果成功則直接返回。如果失敗則說明存在競爭,需要重新add
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
1
2
3
4
5
6
7
8
9
10
11
這裡用到了Cell類物件,Cell物件是LongAdder高併發實現的關鍵。在casBase衝突嚴重的時候,就會去建立Cell物件並新增到cells中,下面會詳細分析。
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
//提供CAS方法修改當前Cell物件上的value
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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
而這一句a = as[getProbe() & m]其實就是透過getProbe()拿到當前Thread的threadLocalRandomProbe的probe Hash值。這個值其實是一個隨機值,這個隨機值由當前執行緒ThreadLocalRandom.current()產生。不用Rondom的原因是因為這裡已經是高併發了,多執行緒情況下Rondom會極大可能得到同一個隨機值。因此這裡使用threadLocalRandomProbe在高併發時會更加隨機,減少衝突。更多ThreadLocalRandom資訊想要深入瞭解可關注這篇文章併發包中ThreadLocalRandom類原理淺嘗。拿到as陣列中當前執行緒的Cell物件,然後再進行CAS的更新操作,我們在原始碼上進行分析。longAccumulate()是在父類Striped64.java中。
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
//如果當前執行緒的隨機數為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;
//如果當前cells陣列不為空
if ((as = cells) != null && (n = as.length) > 0) {
//如果執行緒隨機數對應的cells對應陣列下標的Cell元素不為空,
if ((a = as[(n - 1) & h]) == null) {
//當使用到LongAdder的Cell陣列相關的操作時,需要先獲取全域性的cellsBusy的鎖,才可以進行相關操作。如果當前有其他執行緒的使用,則放棄這一步,繼續for迴圈重試。
if (cellsBusy == 0) { // Try to attach new Cell
//Cell的初始值是x,建立完畢則說明已經加上
Cell r = new Cell(x); // Optimistically create
//casCellsBusy獲取鎖,cellsBusy透過CAS方式獲取鎖,當成功設定cellsBusy為1時,則獲取到鎖。
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 {
//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
//如果a不為空,則對a進行cas增x操作,成功則返回
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
//cells的長度n已經大於CPU數量,則繼續擴容沒有意義,因此直接標記為不衝突
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
//到這一步則說明a不為空但是a上進行CAS操作也有多個執行緒在競爭,因此需要擴容cells陣列,其長度為原長度的2倍
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
}
//繼續使用新的隨機數,避免在同一個Cell上競爭
h = advanceProbe(h);
}
//如果cells為空,則需要先建立Cell陣列。初始長度為2.(個人理解這個if放在前面會比較好一點,哈哈)
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(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
//如果在a上競爭失敗,且擴容競爭也失敗了,則在casBase上嘗試增加數量
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
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
最後是求LongAdder的總數,這一步就非常簡單了,把base的值和所有cells上的value值加起來就是總數了。
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
1
2
3
4
5
6
7
8
9
10
11
思考
原始碼中Cell陣列的會控制在不超過NCPU的兩倍,原因是LongAdder其實在底層是依賴CPU的CAS指令來操作,如果多出太多,即使在程式碼層面沒有競爭,在底層CPU的競爭會更多,所以這裡會有一個數量的限制。所以在LongAdder的設計中,由於使用到CAS指令操作,瓶頸在於CPU上。
YY一下,那麼有沒有方式可以突破這個瓶頸呢?我個人覺得是有的,但是有前提條件,應用場景極其有限。基於ThreadLocal的設計,假設統計只在一個固定的執行緒池中進行,假設執行緒池中的執行緒不會銷燬(異常補充執行緒的就暫時不管了),則可以認為執行緒數量是固定且不變的,那麼統計則可以依賴於只在當前執行緒中進行,那麼即使是高併發,就轉化為ThreadLocal這種單執行緒操作了,完全可以擺脫CAS的CPU指令操作的限制,那麼效能將極大提升。
總結
在併發處理上,AtomicLong和LongAdder均具有各自優勢,需要怎麼使用還是得看使用場景。看完這篇文章,其實並不意味著LongAdder就一定比AtomicLong好使,個人認為在QPS統計等統計操作上,LongAdder會更加適合,而AtomicLong在自增控制方面是LongAdder無法代替的。在多數地併發和少數高併發情況下,AtomicLong和LongAdder效能上差異並不是很大,只有在併發極高的時候,才能真正體現LongAdder的優勢。
---------------------
作者:codingtu
來源:CSDN
原文:https://blog.csdn.net/codingtu/article/details/89047291
版權宣告:本文為博主原創文章,轉載請附上博文連結!
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31554889/viewspace-2640528/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 併發程式設計——多執行緒計數的更優解:LongAdder原理分析程式設計執行緒
- AtomicLong 與 LongAdder(CAS機制的優化)優化
- 從 LongAdder 中窺見併發元件的設計思路元件
- 更簡的併發程式碼,更強的併發控制
- 死磕 java併發包之LongAdder原始碼分析Java原始碼
- 《java併發程式設計的藝術》併發工具類Java程式設計
- 併發程式設計(二)——併發類容器ConcurrentMap程式設計
- Go高效併發 11 | 併發模式:Go 語言中即學即用的高效併發模式Go模式
- 併發工具類(三)控制併發執行緒的數量 Semphore執行緒
- java併發程式設計:Thread類的使用Java程式設計thread
- 簡明高效的 Java 併發程式設計學習指南Java程式設計
- 併發程式設計(一)——同步類容器程式設計
- 使用 .NET Core 高效能併發程式設計程式設計
- 《java併發程式設計的藝術》原子操作類Java程式設計
- 併發工具類
- 併發模型比較模型
- 我從LongAdder中窺探到了高併發的祕籍,上面只寫了兩個字...
- longAdder原理
- LongAdder解析
- 併發工具類(五) Phaser類
- 併發程式設計:DEMO:比較Stream和forkjoin框架的效率程式設計框架
- Go高效併發 08 | 併發基礎:Goroutines 和 Channels 的宣告與使用Go
- 併發工具類——Semaphore
- java併發程式設計-StampedLock高效能讀寫鎖Java程式設計
- 三個好用的併發工具類
- webmen等框架真的比php-fpm併發數高嗎?Web框架PHP
- Go高效併發 10 | Context:多執行緒併發控制神器GoContext執行緒
- 比裁員更侮辱人的事發生了。。。
- 併發程式設計從零開始(十一)-Atomic類程式設計
- JS 物件合併與克隆方法的分類與比較JS物件
- 併發容器、框架、工具類框架
- 計數器方式實現非同步併發非同步
- Java併發程式設計之原子變數Java程式設計變數
- 利用Redis實現高併發計數器Redis
- 平臺運營,讓數智底座更安全更穩定更高效
- Java、Rust、Go、NodeJS、TypeScript併發程式設計比較 - foojayJavaRustGoNodeJSTypeScript程式設計
- java多執行緒與併發 - 併發工具類Java執行緒
- 限制併發數