功能描述
LongAdder通過建立多個副本物件,解決了多執行緒使用CAS更新同一個物件造成的CPU阻塞,加快了對執行緒處理的速度。當多個執行緒同一時刻更新一個AtomicLong型別的變數時,只有一個執行緒能夠更新成功,其他執行緒則更新失敗,繼續嘗試更新。
當使用LongAdder型別的變數時,由於副本陣列的存在,執行緒不一定直接更新變數的本身而是更新副本陣列,這樣多執行緒請求的物件變多了,從而減少了更新時間,當需要使用變數值時,返回的值是基礎變數的值加上陣列內每一個副本的值的和。
原始碼解析
LongAdder繼承自Striped64並實現了Serializable介面,而在Striped64類中有一個Cell類
add方法分析
首先從LongAdder類的add方法入手
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
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);
}
}
從上面的程式碼可以看到LongAdder的實現主要依靠的是cells陣列,如果cells陣列為空的話,則嘗試使用cas更新基礎變數base,如果成功了,則add成功,方法結束,如果cas更新base失敗了,則證明此時有其他執行緒參與base變數的更新,此後的處理與cells不為空一致(如果cells不為空,則在此次方法執行前就已經有多執行緒參與了更新)。
當cells陣列不為空或者更新base變數失敗後,則轉而更新cells陣列中的副本,此時先判斷cells陣列是否為空或長度為0,如果為空或長度為0則說明這是第一次操作cells陣列,應先初始化cells陣列,因此呼叫方法longAccumulate(x, null, true);
如果cells陣列不為空,則嘗試直接訪問陣列中的副本,getProbe方法程式碼:
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
從上面程式碼可以看到getProbe方法獲取了當前執行緒內的PROBE變數,而PROBE定義在Striped64類中
private static final long PROBE;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> sk = Striped64.class;
BASE = UNSAFE.objectFieldOffset
(sk.getDeclaredField("base"));
CELLSBUSY = UNSAFE.objectFieldOffset
(sk.getDeclaredField("cellsBusy"));
Class<?> tk = Thread.class;
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
} catch (Exception e) {
throw new Error(e);
}
}
從上面的程式碼可以看出PROBE來自於Thread類的threadLocalRandomProbe變數,是一個執行緒級變數。getProbe() & m
則是獲取當前要更新的cell,如果cell為空的話則呼叫longAccumulate(x, null, true);
方法設定cell的值,如果cell不為空的話則使用cas直接更新cell的值,並將更新結果儲存在uncontended
中,如果uncontended
的值為false(即cas更新失敗了,此時應該有多個執行緒同時訪問了一個cell),那麼繼續呼叫longAccumulate(x, null, false);
方法。
longAccumulate方法分析
從上面的add方法可以看到,getProbe()
取得了Thread類中的threadLocalRandomProbe變數,而threadLocalRandomProbe變數的初始值為0,因為getProbe()
方法參與了多執行緒訪問哪一個cell的定位,因此getProbe()的值不可能為0,那麼threadLocalRandomProbe變數是在哪裡賦值的呢?
在add方法中觀察到,沒當方法進行不下去時(base變數更新失敗,cells為空,cell更新失敗),都會呼叫longAccumulate
方法,因此longAccumulate
一定是涉及了cells陣列的初始化和擴容,觀察longAccumulate
方法程式碼:
final void longAccumulate(long x, LongBinaryOperator 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(x); // Optimistically create
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) ? v + x :
fn.applyAsLong(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(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
longAccumulate程式碼的設計非常複雜,剛進入方法的程式碼進行了threadLocalRandomProbe變數的初始化
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
如果getProbe()
取得的值為0,說明threadLocalRandomProbe變數並未被初始化過,此時呼叫ThreadLocalRandom.current();
方法進行初始化,並且將引數wasUncontended設定為true。ThreadLocalRandom.current()
方法程式碼:
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
此時發現,threadLocalRandomProbe變數的初始化實在ThreadLocalRandom類中進行的,使用ThreadLocalRandom類的好處是為每一個執行緒維護一個隨機數種子,不涉及多執行緒競爭種子的問題。而在longAccumulate
方法中初始化threadLocalRandomProbe變數是一種延遲初始化的操作,如果cells為空,即使threadLocalRandomProbe變數有值也是沒有意義的。
從上面add
方法呼叫longAccumulate
處可以發現,cells陣列為null或當前執行緒要更新的cell為null時wasUncontended的值為true,如果更新cell失敗,則cell的值為false,那麼wasUncontended的值一定是cells陣列進行擴容的依據。
因為是多個執行緒同時操作cells陣列,那麼對陣列的初始化和擴容一定只能由一個執行緒來完成,因此定義了cellsBusy變數,當檢測到cellsBusy的值為0並使用casCellsBusy()
方法成功將其設定為1後才可以進行初始化和擴容操作。陣列的初始化將cells長度設定為2,並且初始化將要訪問的cell,另一個cell則保持預設值null,完成後將cellsBusy的值重新設定為0,方便其他執行緒之後進行擴容(此處設定並不是cas操作,因為當前初始化程式碼只有一個執行緒能執行到)。
而擴容要檢查當前cells陣列的長度小於cpu的個數時才可以進行(當陣列當都等於cpu個數時效率才最高),擴容操作完成後呼叫advanceProbe()
方法從新計算threadLocalRandomProbe變數的值,以減少訪問celll衝突的個數。
另外,在定義Cell類時使用了@sun.misc.Contended
註解,這樣保證了一個Cell類物件佔滿一個快取行,從而避免了偽共享問題,提升了效能。