淺析LongAdder

淺析LongAdder

前言

上文中分析了AtomicLong以及Unsafe,本文將為大家帶來LongAdder的分析.LongAdder之前在guava以及hystrix等中出現,但是目前已經出現在jdk8標準庫中了,作者是著名的Doug lea大師。

基本分析

先看看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.
<p> 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.
<p>This class extends {@link Number}, but does <em>not</em> 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

留個懸念

  static final class Cell {
        volatile long p0, p1, p2, p3, p4, p5, p6;
        volatile long value;
        volatile long q0, q1, q2, q3, q4, q5, q6;
        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 = getUnsafe();
                Class<?> ak = Cell.class;
                valueOffset = UNSAFE.objectFieldOffset
                    (ak.getDeclaredField("value"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }

    }

Cell單元為啥要這么設計?volatile long p0, p1, p2, p3, p4, p5, p6; volatile long q0, q1, q2, q3, q4, q5, q6;這些看起來沒用的p,q可以刪掉嗎?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容