更勝一籌的LongAdder

AtomicLong

在Java8之前,我們經常使用AtomicLong來表示Long類型的一個原子類。眾所周知,這個類的核心原理就是CAS,也就是比較並交換。CAS利用了CPU指令,使比較和交換兩個操作成為一個原子操作。那麼AtomicLong存在哪些缺陷呢?通過其源碼可知,主要的實現邏輯是,在一個循環中不停地利用CAS設置值,直到CAS執行成功。在較低併發的情況下是沒有問題的。在高併發時,大部分CAS都會執行失敗,這樣就會導致大部分CAS嘗試都是失敗的,CPU升高。

更勝一籌的LongAdder

AtomicLong部分源碼

LongAdder

為了解決上面的問題,在Java8中增加了LongAdder類。這個類的核心思想是將集中的衝突分散,減少併發訪問的衝突。在這個類中,由兩個域來共同表示所代表的數據。一個是long base,另一個是Cell[] cells。

在累加一個值的時候,會有如下的過程:

更勝一籌的LongAdder

LongAdder源碼

1.當cells為空的時候嘗試使用!caseBase(b = base, b + x)把加數加到base上面。在不發生衝突時,流程走到這裡就結束了。

2.如果cells不為空或者caseBase失敗,說明還是存在衝突的。此時獲取數組對應位上的cell,判斷是否為空。不為空,則利用cas嘗試設置。在低併發時,走到這裡就結束了。

3.如果對應位的cell為空或者cas失敗,這個時候就要進入longAccumulate函數了。在這個函數中,全部的邏輯都在一個for循環中:

a.如果對應位沒有cell對象,那麼創建一個cell對象,加入數組。

b.如果嘗試創建新對象失敗,那麼嘗試擴容cells。

c.如果數組大小已經達到上線,設置標誌位,不再擴展。

d.嘗試設置base。

邏輯一直在for裡面循環,直到設置成功為止。

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) { //嘗試創建新的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) //cell的數量已經達到最大了,不再擴容

collide = false; // At max size or stale

else if (!collide)

collide = true;

else if (cellsBusy == 0 && casCellsBusy()) {

try {

if (cells == as) { //擴容cell數組

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

}

}

sum方法

在sum方法裡面,需要疊加base以及cells中全部值。

感想

每個類都要基於怎樣的時候解決怎樣的問題來設計。這個類之所以能夠這麼設計,主要是因為它只提供了累加的功能。如果提供了set方法,那麼就沒有辦法再用這種設計了。沒有萬能的解決方案,凡是要結合實際情況來考慮。我們常提到的性能優化,除了緩存、並行之類的,優化自己的業務邏輯也是很重要的。比如,我們一直吐槽的12306。在最開始,都是安排在統一的一個時間點進行搶票,一瞬間就導致了很高的併發訪問。在把搶票時間分開之後,這種優化效果要比直接通過技術的優化要好很多。


分享到:


相關文章: