從 LongAdder 中窺見併發元件的設計思路

weixin_33912445發表於2018-11-28
4268675-51473e9aa8311a06.jpg
image

原文地址

最近在看阿里的 Sentinel 的原始碼的時候。發現使用了一個類 LongAdder 來在併發環境中計數。這個時候就提出了疑問,JDK 中已經有 AtomicLong 了,為啥還要使用 LongAdder ? AtomicLong 已經是基於 CAS 的無鎖結構,已經有很好的並發表現了,為啥還要用 LongAdder ?於是趕快找來原始碼一探究竟。

AtomicLong 的缺陷

大家可以閱讀我之前寫的 JAVA 中的 CAS 詳細瞭解 AtomicLong 的實現原理。需要注意的一點是,AtomicLong 的 Add() 是依賴自旋不斷的 CAS 去累加一個 Long 值。如果在競爭激烈的情況下,CAS 操作不斷的失敗,就會有大量的執行緒不斷的自旋嘗試 CAS 會造成 CPU 的極大的消耗。

LongAdder 解決方案

通過閱讀 LongAdder 的 Javadoc 我們瞭解到:

This class is usually preferable to {@link AtomicLong} when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization control. Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.

大概意思就是,LongAdder 功能類似 AtomicLong ,在低併發情況下二者表現差不多,在高併發情況下 LongAdder 的表現就會好很多。

LongAdder 到底用了什麼黑科技能做到高性比 AtomicLong 還要好呢呢?對於同樣的一個 add() 操作,上文說到 AtomicLong 只對一個 Long 值進行 CAS 操作。而 LongAdder 是針對 Cell 陣列的某個 Cell 進行 CAS 操作 ,把執行緒的名字的 hash 值,作為 Cell 陣列的下標,然後對 Cell[i] 的 long 進行 CAS 操作。簡單粗暴的分散了高併發下的競爭壓力。

LongAdder 的實現細節

雖然原理簡單粗暴,但是程式碼寫得卻相當細緻和精巧。

java.util.concurrent.atomic 包下面我們可以看到 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)) {
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[getProbe() & m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, null, uncontended);
        }
    }

看到這個 add() 方法,首先需要了解 Cell 是什麼?

Cell 是 java.util.concurrent.atomicStriped64 的一個內部類。

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

首先 Cell 被 @sun.misc.Contended 修飾。意思是讓Java編譯器和JRE執行時來決定如何填充。不理解不要緊,不影響理解。

其實一個 Cell 的本質就是一個 volatile 修飾的 long 值,且這個值能夠進行 cas 操作。

回到我們的 add() 方法。

這裡涉及四個額外的方法 casBase() , getProbe() , a.cas() , longAccumulate();

我們看名字就知道 casBase() 和 a.cas() 都是對引數的 cas 操作。

getProbe() 的作用,就是根據當前執行緒 hash 出一個 int 值。

longAccumlate() 的作用比較複雜,之後我們會講解。

所以這個 add() 操作歸納以後就是:

  1. 如果 cells 陣列不為空,對引數進行 casBase 操作,如果 casBase 操作失敗。可能是競爭激烈,進入第二步。
  2. 如果 cells 為空,直接進入 longAccumulate();
  3. m = cells 陣列長度減一,如果陣列長度小於 1,則進入 longAccumulate()
  4. 如果都沒有滿足以上條件,則對當前執行緒進行某種 hash 生成一個陣列下標,對下標儲存的值進行 cas 操作。如果操作失敗,則說明競爭依然激烈,則進入 longAccumulate().

可見,操作的核心思想還是基於 cas。但是 cas 失敗後,並不是傻乎乎的自旋,而是逐漸升級。升級的 cas 都不管用了則進入 longAccumulate() 這個方法。

下面就開始揭開 longAccumulate 的神祕面紗。

    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;
            //如果操作的cell 為空,double check 新建 cell
            if ((as = cells) != null && (n = as.length) > 0) {
                if ((a = as[(n - 1) & h]) == null) {
                    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;
                }

                // cas 失敗 繼續迴圈
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash

                // 如果 cell cas 成功 break
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;

                // 如果 cell 的長度已經大於等於 cpu 的數量,擴容意義不大,就不用標記衝突,重試
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                // 獲取鎖,上鎖擴容,將衝突標記為否,繼續執行    
                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
                }
                // 沒法獲取鎖,重雜湊,嘗試其他槽
                h = advanceProbe(h);
            }

            // 獲取鎖,初始化 cell 陣列
            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;
            }

            // 表未被初始化,可能正在初始化,回退使用 base。
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

longAccumulate 看上去比較複雜。我們慢慢分析。

回憶一下,什麼情況會進入到這個 longAccumulate 方法中,

  • cell[] 陣列為空,
  • cell[i] 資料的某個下標元素為空,
  • casBase 失敗,
  • a.cas 失敗,
  • cell.length - 1 < 0

在 longAccumulate 中有幾個標記位,我們也先理解一下

  • cellsBusy cells 的操作標記位,如果正在修改、新建、操作 cells 陣列中的元素會,會將其 cas 為 1,否則為0。
  • wasUncontended 表示 cas 是否失敗,如果失敗則考慮操作升級。
  • collide 是否衝突,如果衝突,則考慮擴容 cells 的長度。

整個 for(;;) 死迴圈,都是以 cas 操作成功而告終。否則則會修改上述描述的幾個標記位,重新進入迴圈。

所以整個迴圈包括如下幾種情況:

  1. cells 不為空

    1. 如果 cell[i] 某個下標為空,則 new 一個 cell,並初始化值,然後退出
    2. 如果 cas 失敗,繼續迴圈
    3. 如果 cell 不為空,且 cell cas 成功,退出
    4. 如果 cell 的數量,大於等於 cpu 數量或者已經擴容了,繼續重試。(擴容沒意義)
    5. 設定 collide 為 true。
    6. 獲取 cellsBusy 成功就對 cell 進行擴容,獲取 cellBusy 失敗則重新 hash 再重試。
  2. cells 為空且獲取到 cellsBusy ,init cells 陣列,然後賦值退出。

  3. cellsBusy 獲取失敗,則進行 baseCas ,操作成功退出,不成功則重試。

至此 longAccumulate 就分析完了。之所以這個方法那麼複雜,我認為有兩個原因

  1. 是因為併發環境下要考慮各種操作的原子性,所以對於鎖都進行了 double check。
  2. 操作都是逐步升級,以最小的代價實現功能。

最後說說 LongAddr 的 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;
    }

就是遍歷 cell 陣列,累加 value 就行。LongAdder 餘下的方法就比較簡單,沒有什麼可以討論的了。

LongAdder VS AtomicLong

看上去 LongAdder 效能全面超越了 AtomicLong。為什麼 jdk 1.8 中還是保留了 AtomicLong 的實現呢?

其實我們可以發現,LongAdder 使用了一個 cell 列表去承接併發的 cas,以提升效能,但是 LongAdder 在統計的時候如果有併發更新,可能導致統計的資料有誤差。

如果用於自增 id 的生成,就不適合使用 LongAdder 了。這個時候使用 AtomicLong 就是一個明智的選擇。

而在 Sentinel 中 LongAdder 承擔的只是統計任務,且允許誤差。

總結

LongAdder 使用了一個比較簡單的原理,解決了 AtomicLong 類,在極高競爭下的效能問題。但是 LongAdder 的具體實現卻非常精巧和細緻,分散競爭,逐步升級競爭的解決方案,相當漂亮,值得我們細細品味。

歡迎關注我的微信公眾號


4268675-b0309473163f6de2.jpg
二維碼

相關文章