Java併發包-淺析LongAdder

淺析LongAdder

基本分析

先看看LongAdder的java doc的描述:

One or more variables that together maintain an initially zero {@code
long} sum. When updates (method {@link #add}) are contended across
threads, the set of variables may grow dynamically to reduce
contention. Method {@link #sum} (or, equivalently, {@link longValue})
returns the current total combined across the variables maintaining
the sum. 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. This class extends {@link Number}, but does not define
methods such as {@code hashCode} and {@code compareTo} because
instances are expected to be mutated, and so are not useful as
collection keys. jsr166e note: This class is targeted to be placed in
java.util.concurrent.atomic

翻譯過來就是說:

LongAdder中會維護一個或多個變量,這些變量共同組成一個long型的“和”。當多個線程同時更新(特指“add”)值時,爲了減少競爭,可能會動態地增加這組變量的數量。“sum”方法(等效於longValue方法)返回這組變量的“和”值。
當我們的場景是爲了統計技術,而不是爲了更細粒度的同步控制時,並且是在多線程更新的場景時,LongAdder類比AtomicLong更好用。
在小併發的環境下,論更新的效率,兩者都差不多。但是高併發的場景下,LongAdder有着明顯更高的吞吐量,但是有着更高的空間複雜度。
從上面的java
doc來看,LongAdder有兩大方法,add和sum。其更適合使用在多線程統計計數的場景下,在這個限定的場景下比AtomicLong要高效一些,下面我們來分析下爲啥在這種場景下LongAdder會更高效。

add方法

public void add(long x) {
        Cell[] as; long b, v; HashCode hc; Cell a; int n;
        //首先判斷cells是否還沒被初始化,並且嘗試對value值進行cas操作
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            //查看當前線程中HashCode中存儲的隨機值
            int h = (hc = threadHashCode.get()).code;
            //此處有多個判斷條件,依次是
            //1.cell[]數組還未初始化
            //2.cell[]數組雖然初始化了但是數組長度爲0
            //3.該線程所對應的cell爲null,其中要注意的是,當n爲2的n次冪時,((n - 1) & h)等效於h%n
            //4.嘗試對該線程對應的cell單元進行cas更新(加上x)
            if (as == null || (n = as.length) < 1 ||
                (a = as[(n - 1) & h]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                //在以上條件都失效的情況下,重試update
                retryUpdate(x, hc, uncontended);
        }
    }

    //一個ThreadLocal類
    static final class ThreadHashCode extends ThreadLocal<HashCode> {
        public HashCode initialValue() { return new HashCode(); }
    }


    static final ThreadHashCode threadHashCode = new ThreadHashCode();


    //每個HashCode在初始化時會產生並保存一個非0的隨機數
    static final class HashCode {
        static final Random rng = new Random();
        int code;
        HashCode() {
            int h = rng.nextInt(); // Avoid zero to allow xorShift rehash
            code = (h == 0) ? 1 : h;
        }
    }

    //嘗試使用casBase對value值進行update,baseOffset是value相對於LongAdder對象初始位置的內存偏移量
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, baseOffset, cmp, val);
    }

add方法的註釋在其中,讓我們再看看重要的retryUpdate方法。

retryUpdate方法

在上述四個條件都失敗的情況下嘗試再次update,我們猜測在四個條件都失敗的情況下在retryUpdate中肯定都對應四個條件失敗的處理方法,並且update一定要成功,所以肯定有相應的循環+cas的方式出現。

final void retryUpdate(long x, HashCode hc, boolean wasUncontended) {
        int h = hc.code;
        boolean collide = false;                // True if last slot nonempty
        //我們猜測的for循環
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            //這個if分支處理上述四個條件中的3和4,此時cells數組已經初始化了並且長度大於0
            if ((as = cells) != null && (n = as.length) > 0) {
                //該分支處理四個條件中的3分支,線程對應的cell爲null
                if ((a = as[(n - 1) & h]) == null) {
                    //如果busy鎖沒被佔有
                    if (busy == 0) {            // Try to attach new Cell
                          //新建一個cell
                        Cell r = new Cell(x);   // Optimistically create
                        //double check busy,並且嘗試鎖busy(樂觀鎖)
                        if (busy == 0 && casBusy()) {
                            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) {
                                    //再次確認線程hashcode所對應的cell爲null,將新建的cell賦值
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                            //解鎖
                                busy = 0;
                            }
                            if (created)
                                break;
                            //如果失敗,再次嘗試
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                //處理四個條件中的條件4,置爲true後交給循環重試
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //嘗試給線程對應的cell update
                else if (a.cas(v = a.value, fn(v, x)))
                    break;
                else if (n >= NCPU || cells != as)
                    collide = false;            // At max size or stale
                else if (!collide)
                    collide = true;
                //在以上辦法都不管用的情況下嘗試擴大cell
                else if (busy == 0 && casBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                        //擴大一倍,將前N個拷貝過去
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        busy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                //rehash下,走到這一步基本是因爲多個線程的競爭太激烈了,所以在擴展cell後rehash h,等待下次循環處理好這次更新
                h ^= h << 13;                   // Rehash
                h ^= h >>> 17;
                h ^= h << 5;
            }
            //主要針對上述四個條件中的1.2,此時cells還未進行第一次初始化,其中casBusy的理解參照下面busy的      註釋,如果casBusy能成功才進入這個分支
            else if (busy == 0 && cells == as && casBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        //創建數量爲2的cell數組,2很重要,因爲每次都是n<<1進行擴大一倍的,所以n永遠是2的冪
                        Cell[] rs = new Cell[2];
                        //需要注意的是h&1 = h%2,將線程對應的cell初始值設置爲x
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                //釋放busy鎖
                    busy = 0;
                }
                if (init)
                    break;
            }
            //busy鎖不成功或者忙,則再重試一次casBase對value直接累加
            else if (casBase(v = base, fn(v, x)))
                break;                          // Fall back on using base
        }
        hc.code = h;                            // Record index for next time
    }

    /**
     * Spinlock (locked via CAS) used when resizing and/or creating Cells.
     通過cas實現的自旋鎖,用於擴大或者初始化cells
     */
    transient volatile int busy;

從以上分析來看,retryUpdate非常的複雜,所做的努力就是爲了儘量減少多個線程更新同一個值value,能用簡單的方式解決的絕對不採用開銷更大的方法(resize cell也是走投無路的時候)
回過頭來總結分析下LongAdder減少衝突的方法以及在求和場景下比AtomicLong更高效的原因
● 首先和AtomicLong一樣,都會先採用cas方式更新值
● 在初次cas方式失敗的情況下(通常證明多個線程同時想更新這個值),嘗試將這個值分隔成多個cell(sum的時候求和就好),讓這些競爭的線程只管更新自己所屬的cell(因爲在rehash之前,每個線程中存儲的hashcode不會變,所以每次都應該會找到同一個cell),這樣就將競爭壓力分散了

sum方法

public long sum() {
        long sum = base;
        Cell[] as = cells;
        if (as != null) {
            int n = as.length;
            for (int i = 0; i < n; ++i) {
                Cell a = as[i];
                if (a != null)
                    sum += a.value;
            }
        }
        return sum;
    }

sum方法就簡單多了,將cell數組中的value求和就好
AtomicLong可否可以被LongAdder替代
有了傳說中更高效的LongAdder,那AtomicLong可否不使用了呢?當然不是!
答案就在LongAdder的java doc中,從我們翻譯的那段可以看出,LongAdder適合的場景是統計求和計數的場景,而且LongAdder基本只提供了add方法,而AtomicLong還具有cas方法(要使用cas,在不直接使用unsafe之外只能藉助AtomicXXX了)
LongAdder有啥用
從java doc中可以看出,其適用於統計計數的場景,例如計算qps這種場景。在高併發場景下,qps這個值會被多個線程頻繁更新的,所以LongAdder很適合。HystrixRollingNumber就是用了它,下篇文章介紹它
總結
本文簡單分析了下LongAdder,下篇文章介紹HystrixRollingNumber

作者:LNAmp
鏈接:http://www.jianshu.com/p/22d38d5c8c2a
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

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