淺析LongAdder

淺析LongAdder

前言

上文中分析了AtomicLong以及Unsafe,本文將為大家?guī)?lái)LongAdder的分析.LongAdder之前在guava以及hystrix等中出現(xiàn),但是目前已經(jīng)出現(xiàn)在jdk8標(biāo)準(zhǔn)庫(kù)中了,作者是著名的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

翻譯過(guò)來(lái)就是說(shuō):
LongAdder中會(huì)維護(hù)一個(gè)或多個(gè)變量,這些變量共同組成一個(gè)long型的“和”。當(dāng)多個(gè)線程同時(shí)更新(特指“add”)值時(shí),為了減少競(jìng)爭(zhēng),可能會(huì)動(dòng)態(tài)地增加這組變量的數(shù)量。“sum”方法(等效于longValue方法)返回這組變量的“和”值。
當(dāng)我們的場(chǎng)景是為了統(tǒng)計(jì)技術(shù),而不是為了更細(xì)粒度的同步控制時(shí),并且是在多線程更新的場(chǎng)景時(shí),LongAdder類比AtomicLong更好用。 在小并發(fā)的環(huán)境下,論更新的效率,兩者都差不多。但是高并發(fā)的場(chǎng)景下,LongAdder有著明顯更高的吞吐量,但是有著更高的空間復(fù)雜度。

從上面的java doc來(lái)看,LongAdder有兩大方法,add和sum。其更適合使用在多線程統(tǒng)計(jì)計(jì)數(shù)的場(chǎng)景下,在這個(gè)限定的場(chǎng)景下比AtomicLong要高效一些,下面我們來(lái)分析下為啥在這種場(chǎng)景下LongAdder會(huì)更高效。

add方法

public void add(long x) {
        Cell[] as; long b, v; HashCode hc; Cell a; int n;
        //首先判斷cells是否還沒(méi)被初始化,并且嘗試對(duì)value值進(jìn)行cas操作
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            //查看當(dāng)前線程中HashCode中存儲(chǔ)的隨機(jī)值
            int h = (hc = threadHashCode.get()).code;
            //此處有多個(gè)判斷條件,依次是
            //1.cell[]數(shù)組還未初始化
            //2.cell[]數(shù)組雖然初始化了但是數(shù)組長(zhǎng)度為0
            //3.該線程所對(duì)應(yīng)的cell為null,其中要注意的是,當(dāng)n為2的n次冪時(shí),((n - 1) & h)等效于h%n
            //4.嘗試對(duì)該線程對(duì)應(yīng)的cell單元進(jìn)行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);
        }
    }

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

    
    static final ThreadHashCode threadHashCode = new ThreadHashCode();


    //每個(gè)HashCode在初始化時(shí)會(huì)產(chǎn)生并保存一個(gè)非0的隨機(jī)數(shù)
    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對(duì)value值進(jìn)行update,baseOffset是value相對(duì)于LongAdder對(duì)象初始位置的內(nèi)存偏移量
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, baseOffset, cmp, val);
    }

add方法的注釋在其中,讓我們?cè)倏纯粗匾膔etryUpdate方法。retryUpdate在上述四個(gè)條件都失敗的情況下嘗試再次update,我們猜測(cè)在四個(gè)條件都失敗的情況下在retryUpdate中肯定都對(duì)應(yīng)四個(gè)條件失敗的處理方法,并且update一定要成功,所以肯定有相應(yīng)的循環(huán)+cas的方式出現(xiàn)。

final void retryUpdate(long x, HashCode hc, boolean wasUncontended) {
        int h = hc.code;
        boolean collide = false;                // True if last slot nonempty
        //我們猜測(cè)的for循環(huán)
        for (;;) {
            Cell[] as; Cell a; int n; long v;
            //這個(gè)if分支處理上述四個(gè)條件中的3和4,此時(shí)cells數(shù)組已經(jīng)初始化了并且長(zhǎng)度大于0
            if ((as = cells) != null && (n = as.length) > 0) {
                //該分支處理四個(gè)條件中的3分支,線程對(duì)應(yīng)的cell為null
                if ((a = as[(n - 1) & h]) == null) {
                    //如果busy鎖沒(méi)被占有
                    if (busy == 0) {            // Try to attach new Cell
                        //新建一個(gè)cell
                        Cell r = new Cell(x);   // Optimistically create
                        //double check busy,并且嘗試鎖busy(樂(lè)觀鎖)
                        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) {
                                    //再次確認(rèn)線程hashcode所對(duì)應(yīng)的cell為null,將新建的cell賦值
                                    rs[j] = r;
                                    created = true;
                                }
                            } finally {
                            //解鎖
                                busy = 0;
                            }
                            if (created)
                                break;
                            //如果失敗,再次嘗試
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                //處理四個(gè)條件中的條件4,置為true后交給循環(huán)重試
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                //嘗試給線程對(duì)應(yīng)的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;
                //在以上辦法都不管用的情況下嘗試擴(kuò)大cell
                else if (busy == 0 && casBusy()) {
                    try {
                        if (cells == as) {      // Expand table unless stale
                        //擴(kuò)大一倍,將前N個(gè)拷貝過(guò)去
                            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下,走到這一步基本是因?yàn)槎鄠€(gè)線程的競(jìng)爭(zhēng)太激烈了,所以在擴(kuò)展cell后rehash h,等待下次循環(huán)處理好這次更新
                h ^= h << 13;                   // Rehash
                h ^= h >>> 17;
                h ^= h << 5;
            }
            //主要針對(duì)上述四個(gè)條件中的1.2,此時(shí)cells還未進(jìn)行第一次初始化,其中casBusy的理解參照下面busy的      注釋,如果casBusy能成功才進(jìn)入這個(gè)分支
            else if (busy == 0 && cells == as && casBusy()) {
                boolean init = false;
                try {                           // Initialize table
                    if (cells == as) {
                        //創(chuàng)建數(shù)量為2的cell數(shù)組,2很重要,因?yàn)槊看味际莕<<1進(jìn)行擴(kuò)大一倍的,所以n永遠(yuǎn)是2的冪
                        Cell[] rs = new Cell[2];
                        //需要注意的是h&1 = h%2,將線程對(duì)應(yīng)的cell初始值設(shè)置為x
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        init = true;
                    }
                } finally {
                //釋放busy鎖
                    busy = 0;
                }
                if (init)
                    break;
            }
            //busy鎖不成功或者忙,則再重試一次casBase對(duì)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.
     通過(guò)cas實(shí)現(xiàn)的自旋鎖,用于擴(kuò)大或者初始化cells
     */
    transient volatile int busy;

從以上分析來(lái)看,retryUpdate非常的復(fù)雜,所做的努力就是為了盡量減少多個(gè)線程更新同一個(gè)值value,能用簡(jiǎn)單的方式解決的絕對(duì)不采用開(kāi)銷更大的方法(resize cell也是走投無(wú)路的時(shí)候)

回過(guò)頭來(lái)總結(jié)分析下LongAdder減少?zèng)_突的方法以及在求和場(chǎng)景下比AtomicLong更高效的原因

  • 首先和AtomicLong一樣,都會(huì)先采用cas方式更新值
  • 在初次cas方式失敗的情況下(通常證明多個(gè)線程同時(shí)想更新這個(gè)值),嘗試將這個(gè)值分隔成多個(gè)cell(sum的時(shí)候求和就好),讓這些競(jìng)爭(zhēng)的線程只管更新自己所屬的cell(因?yàn)樵趓ehash之前,每個(gè)線程中存儲(chǔ)的hashcode不會(huì)變,所以每次都應(yīng)該會(huì)找到同一個(gè)cell),這樣就將競(jìng)爭(zhēng)壓力分散了

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方法就簡(jiǎn)單多了,將cell數(shù)組中的value求和就好

AtomicLong可否可以被LongAdder替代

有了傳說(shuō)中更高效的LongAdder,那AtomicLong可否不使用了呢?當(dāng)然不是!

答案就在LongAdder的java doc中,從我們翻譯的那段可以看出,LongAdder適合的場(chǎng)景是統(tǒng)計(jì)求和計(jì)數(shù)的場(chǎng)景,而且LongAdder基本只提供了add方法,而AtomicLong還具有cas方法(要使用cas,在不直接使用unsafe之外只能借助AtomicXXX了)

LongAdder有啥用

從java doc中可以看出,其適用于統(tǒng)計(jì)計(jì)數(shù)的場(chǎng)景,例如計(jì)算qps這種場(chǎng)景。在高并發(fā)場(chǎng)景下,qps這個(gè)值會(huì)被多個(gè)線程頻繁更新的,所以LongAdder很適合。HystrixRollingNumber就是用了它,下篇文章介紹它

總結(jié)

本文簡(jiǎn)單分析了下LongAdder,下篇文章介紹HystrixRollingNumber

留個(gè)懸念

  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單元為啥要這么設(shè)計(jì)?volatile long p0, p1, p2, p3, p4, p5, p6; volatile long q0, q1, q2, q3, q4, q5, q6;這些看起來(lái)沒(méi)用的p,q可以刪掉嗎?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 175,409評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,569評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 54,895評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,123評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,363評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,622評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,354評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容