AtomicLong
在Java8之前,我們經常使用AtomicLong來表示Long類型的一個原子類。眾所周知,這個類的核心原理就是CAS,也就是比較並交換。CAS利用了CPU指令,使比較和交換兩個操作成為一個原子操作。那麼AtomicLong存在哪些缺陷呢?通過其源碼可知,主要的實現邏輯是,在一個循環中不停地利用CAS設置值,直到CAS執行成功。在較低併發的情況下是沒有問題的。在高併發時,大部分CAS都會執行失敗,這樣就會導致大部分CAS嘗試都是失敗的,CPU升高。
LongAdder
為了解決上面的問題,在Java8中增加了LongAdder類。這個類的核心思想是將集中的衝突分散,減少併發訪問的衝突。在這個類中,由兩個域來共同表示所代表的數據。一個是long base,另一個是Cell[] cells。
在累加一個值的時候,會有如下的過程:
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。在最開始,都是安排在統一的一個時間點進行搶票,一瞬間就導致了很高的併發訪問。在把搶票時間分開之後,這種優化效果要比直接通過技術的優化要好很多。
閱讀更多 java技術小碼農 的文章