LongAdder類實現原理、源碼解析

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),如果下次還是執行到了拓容部分,則再拓容,從而儘量減少不必要的拓容操作。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章