1. 概述
AtomicLong通過循環CAS實現原子操作,
缺點是當高併發下競爭比較激烈的時候,會出現大量的CAS失敗,導致循環CAS次數大大增加,這種自旋是要消耗時間cpu時間片的,會佔用大量cpu的時間,降低效率。
那這個問題如何解決呢?
JUC給我們提供了一個類,LongAdder, 它的作用和AtomicLong是一樣的,都是一個實現了原子操作的累加器,
LongAdder通過維護一個基準值base和 Cell 數組,多線程的時候多個線程去競爭Cell數組的不同的元素,進行cas累加操作,並且每個線程競爭的Cell的下標不是固定的,如果CAS失敗,會重新獲取新的下標去更新,從而極大地減少了CAS失敗的概率,最後在將base 和 Cell數組的元素累加,獲取到我們需要的結果。
2. 涉及到的類、變量、方法
變量:
- Striped64(LongAdder的父類)中維護者三個變量:base、cellsBusy、Cell數組 ,都用 volatile修飾
- base 爲基本數據,在線程競爭比較少的時候,cas更新base,sum()返回的結果也爲base;
- cellsBusy 是一把鎖,值爲0或者1,1表示Cell數組正在進行初始化或者拓容操作;
- Cell[] 數組 線程競爭的數據就是這個數組的元素,size爲2的整數次冪(初始化size爲2,後面每次拓容都是是原來的size 左移一位)
類:
- Cell 類,Striped64的內部類,這個類和AtomicLong很像,是線程競爭的具體對象的類,提供了cas更新其value的方法,並且使用@sun.misc.Contended修飾,用來防止僞共享。
@sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } 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); } } }
方法:
- add(long x); LongAdder的add累加方法
- sum(); LongAdder的獲取累加結果的方法,由於在sum中沒有任何的加鎖的操作,在遍歷獲取cells中的值的時候,可能有線程在拓容累加等操作,所以sum返回的值並不精確,不是一個原子快照
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; }
- casBase(long cmp, long val) ; 更新base的CAS方法
- getProbe(); 獲取線程的探針值,用於計算線程操作的cells數組的下標
- advanceProbe(int probe) ; 修改線程的探針值,用於cas失敗的時候不會再去循環等待某個元素,而重新獲取新的元素去競爭
- longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended);下面會詳細說明
3. 代碼解釋
調用add 方法:
(1)邏輯爲: cells 爲null 並且cas更新base成功,則直接返回結果不進行後續判斷,
(3)(4)(5) 邏輯爲: cells !=null && as[getProbe() & m] != null && cas更新as[getProbe() & m] 成功,則直接結束,否則就會執行longAccumulate(x, null, uncontended)方法
(2)(5) uncontended 用來記錄cas更新cell元素失敗
public void add(long x) { Cell[] as; long b, v; int m; Cell a; if ((as = cells) != null || !casBase(b = base, b + x)) { //(1) boolean uncontended = true; //(2) if (as == null || (m = as.length - 1) < 0 || //(3) (a = as[getProbe() & m]) == null || //(4) !(uncontended = a.cas(v = a.value, v + x))) //(5) longAccumulate(x, null, uncontended); } }
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { // 用於通過 h & cells.length -1 計算線程操作的位置 int h; // 判斷線程的probe是否初始化 if ((h = getProbe()) == 0) { // 初始化線程probe ThreadLocalRandom.current(); // force initialization h = getProbe(); wasUncontended = true; } // 用於記錄cell數組當前是否存在空的桶,false表示大概率存在空桶,及時滿足拓容條件也暫時先不拓容 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) { if ((a = as[(n - 1) & h]) == null) { // cells 不爲空,並且線程操作桶爲null,則new Cell(x)並更新 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; } // 在LongAdder.add()方法中已經cas過一次,如果probe沒有更新,則直接進行a.cas操作大概率失敗,則加了此次判斷 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; // cell數組長度大於cpu數,則後續不再拓容 // cells != as 正在拓容,則下次循環有空桶的概率較大 將collide = false,下次執行到此處則會advanceProbe(h) 一次而非直接拓容 else if (n >= NCPU || cells != as) collide = false; // At max size or stale else if (!collide) collide = true; // 拓容,每次爲 數組長度 << 1 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 } // 每次失敗後都會修改probe值,重新進入循環,而非probe不變 h = advanceProbe(h); } // cells 爲null,則初始化cells,初始size爲2 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; } // cells 爲null ,並且cellsBusy 鎖競爭失敗,則其他線程正在初始化,嘗試casBase else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // Fall back on using base } }
我整理了一個詳細的邏輯流程圖:
4. 優缺點
高併發下不會循環cas,是的在高併發情況下效率比AtomicLong高,但是sum()方法返回值並不完全精確,
所以可以用在併發要求比較高,但是結果精度要求不是特別搞得情況,如QPS的統計等場景,
在工作中可能應用場景不會很多,但是我們學習的目的是更多的事爲了學習其實現的原理和思路,
Doug Lea 在寫JUC包的時候的一些解決問題的思想和方式,幫助我們理解其他的併發類,也爲我們自己的代碼編寫提供參考。
5. 問題思考
- cells 是如何解決僞共享問題的 :
- 通過@sun.misc.Contended 註解強制Cells數組的單個元素獨佔cpu緩存行,進而避免僞共享
- a = as[getProbe() & (cells.length -1)] 爲何使用這樣的計算方式:
- cells 的length 爲 2的整數次冪,當 m 爲 2的整數冪的時候, a % m 和 a & (m-1) 的結果是一樣的,而且 & 是位運算比 %運算高很多,所以以這種方式計算線程操作cells的下標速度會很快,cells的長度之所以設計爲2的整數次冪也是這個原因
- wasUncontended 的作用是什麼:
- 減少二次競爭,wasUncontended =false 則說明最近一次cas失敗了,繼續執行也大概率事變,所以已經知道cas爲 fail了就直接先執行h = advanceProbe(h),進行下一次循環,這樣也是體現了JUC作者對性能的極致追求了,不放過任何一點的可能優化的空間;
- collide 的作用:
- 不加這個標識,執行到拓容的代碼部分時,就直接拓容了,但是,在上個循環中如果我們剛拓容的或者正在拓容,或者剛判斷出有空的cell,這次循環中cells是大概率有空桶的並不需要立即拓容,所以這個標識的作用是再給一次機會,wasUncontended = true; h = advanceProbe(h),如果下次還是執行到了拓容部分,則再拓容,從而儘量減少不必要的拓容操作。