Java8 源碼閱讀 - LongAdder

LongAdder和AtomicLong類似是用于多線程下來保證數據更新的原子性,AtomicLong主要是依賴CAS操作來保證原子性的,其方法本質是在循環中一直嘗試CAS,直到成功時才退出循環,所以在線程競爭激烈的場景往往性能不是很好(盡管已經比使用悲觀鎖好的多);

LongAdder采用的是類似分治的思想,再遇到多個線程同時對數據進行更新時,會將數據分為多份更細粒度的子單位再更新,從而達到減少線程競爭的目的,額外的消耗是需要更多的空間;在ConcurrentHashMap中也有它的身影,

源碼實現

Striped64是一個抽象類,里面的實現是LongAdder操作方法的基礎;該類里面維護了一個表只允許原子性操作并且是懶加載的,大小為2的冪次方;

 @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);
            }
        }
    }

AtomicLong類的變體,只支持原始訪問和CAS操作,這個就是LongAdder中細粒度的單位;

/** cpu的數目,決定著細分cell的數量 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

/** cells表,當非空時size是2的冪 */
transient volatile Cell[] cells;

/** 基本值,主要在沒有競爭時使用,通過CAS更新 */
transient volatile long base;

/** 自旋鎖,通過CAS鎖定 */
transient volatile int cellsBusy;

/**  可以看到更新BASE時是用cas操作 */
final boolean casBase(long cmp, long val) {
    return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}

/** 從0到1的情況表示獲取鎖 把cellsBusy置0表示釋放鎖*/
final boolean casCellsBusy() {
    return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}

base屬性就是沒有線程沖突時累加的數值,在遇到線程競爭時才進行分治處理;

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) {
            // 當h值為0時表示未初始化 
            ThreadLocalRandom.current(); // 強制初始化
            h = getProbe();
            wasUncontended = true;
        }
        //如果最后一個槽非空,則為真,也用于控制擴容,false重試。
        boolean collide = false;  
        for (;;) { //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);   // 樂觀地創建
                        if (cellsBusy == 0 && casCellsBusy()) {
                            // 鎖仍然是空閑的、且成功獲取到鎖
                            boolean created = false;
                            try {     
                                // 在持有鎖時再次檢查槽是否空閑
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null && //如果cells不為空
                                    (m = rs.length) > 0 &&
                                    rs[j = (m - 1) & h] == null) {
                                    // 所映射的槽仍為空
                                    rs[j] = r;// 關聯 cell 到槽
                                    created = true;
                                }
                            } finally {
                                cellsBusy = 0;// 釋放鎖
                            }
                            if (created)
                                break;// 成功創建cell并關聯到槽,退出循環
                            continue;  //走到這表示上面獲取到鎖時槽被占用了 需要重新循環申請鎖
                        }
                    }
                    collide = false;// 鎖被占用了,重試
                }
                // 槽被占用了
                else if (!wasUncontended)       // 已知CAS失敗
                    wasUncontended = true;      // 在重散列后繼續,在當前槽的cell上嘗試更新,重裝散列表示重新刷新h值
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;;
                    // 表達到上限后(最大為CPU核數)就不會再嘗試下面if的擴容了,只會重散列,嘗試其他槽
                else if (n >= NCPU || cells != as)
                    collide = false;    
                else if (!collide)
                    collide = true;
                    // 如果不存在沖突,則設置為存在沖突
                else if (cellsBusy == 0 && casCellsBusy()) {
                    // 鎖空閑且成功獲取到鎖
                    // 進到這里表示沒有足夠的槽添加了 需要進行擴容
                    try {
                        if (cells == as) {      //  距上一次檢查后表沒有被改變,進行擴容
                            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;                   // 在擴容后的表上重試
                }
                // 沒法獲取鎖,重散列,嘗試其他槽
                h = advanceProbe(h);
            }
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                //未初始化表,在獲取鎖成功后初始化表
                boolean init = false;
                try {       
                    if (cells == as) {
                        Cell[] rs = new Cell[2]; //初始化大小為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))))
                //表未被初始化,可能正在被初始化,
                //直接嘗試cas設置base值,如果本次cas成功則退出循環,否則重新判斷
                break;                          
        }
    }

上面這段代碼就是LongAdder類處理多線程沖突下的分治操作,總體而言操作流程如下;

  1. 發生線程競爭,判斷是否初始化過Table;如未初始化,會創建一個容量為2的Table,并且將value插入其中一個插槽;
  2. 第二次發生線程競爭時,會先根據ThreadLocalRandom的探針計算哈希值來尋找插槽,如果插槽為空,則插入插槽,如果插槽不為空,會嘗試通過CAS更新該插槽的值;
  3. 如果插槽不為空且CAS更新失敗,則會嘗試擴建Table,最多擴張到大于或等于最接近CPU核數的2的冪次方;

通過ThreadLocalRandom的探針字段來用于每個線程的哈希碼,為0意味著未初始化,發生線程沖突時,如果Table容量不能再擴大,且CAS操作失敗,則會進行雙重哈希,使用輔助哈希Marsaglia XorShift嘗試查找空閑插槽;

//Marsaglia XorShif隨機數算法
static final int advanceProbe(int probe) {
        probe ^= probe << 13;   // xorshift
        probe ^= probe >>> 17;
        probe ^= probe << 5;
        UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
        return probe;
}

回到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)) {
            // cells 不為空 或更新base的cas失敗,也即出現了競爭。
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 || // as 為空表示cells未被初始化 || cells的長度為0
                (a = as[getProbe() & m]) == null || // as[i]上的值為null,表示該位置沒有被占用
                !(uncontended = a.cas(v = a.value, v + x))) //cas 成功,將as[i]的值替換成 oldvalue + x
                // 如果所映射的槽不為空,且成功更新則返回,否則進入復雜處理流程。
                longAccumulate(x, null, uncontended);
        }
}

可以看出在沒有線程沖突時還是會先通過cas更新base值,極力避免進到Striped64的復雜處理流程;

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;
}

sum聚合操作就是將base值和Table中所有Cell值相加得出,下圖可能有點不準確,但是思想是類似的;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容