Java 并發(fā)計數(shù)組件Striped64詳解

作者: 一字馬胡
轉(zhuǎn)載標(biāo)志 【2017-11-03】

更新日志

日期 更新內(nèi)容 備注
2017-11-03 添加轉(zhuǎn)載標(biāo)志 持續(xù)更新

Java Striped64

Striped64是在java8中添加用來支持累加器的并發(fā)組件,它可以在并發(fā)環(huán)境下使用來做某種計數(shù),Striped64的設(shè)計思路是在競爭激烈的時候盡量分散競爭,在實現(xiàn)上,Striped64維護了一個base Count和一個Cell數(shù)組,計數(shù)線程會首先試圖更新base變量,如果成功則退出計數(shù),否則會認(rèn)為當(dāng)前競爭是很激烈的,那么就會通過Cell數(shù)組來分散計數(shù),Striped64根據(jù)線程來計算哈希,然后將不同的線程分散到不同的Cell數(shù)組的index上,然后這個線程的計數(shù)內(nèi)容就會保存在該Cell的位置上面,基于這種設(shè)計,最后的總計數(shù)需要結(jié)合base以及散落在Cell數(shù)組中的計數(shù)內(nèi)容。這種設(shè)計思路類似于java7的ConcurrentHashMap實現(xiàn),也就是所謂的分段鎖算法,ConcurrentHashMap會將記錄根據(jù)key的hashCode來分散到不同的segment上,線程想要操作某個記錄只需要鎖住這個記錄對應(yīng)著的segment就可以了,而其他segment并不會被鎖住,其他線程任然可以去操作其他的segment,這樣就顯著提高了并發(fā)度,雖然如此,java8中的ConcurrentHashMap實現(xiàn)已經(jīng)拋棄了java7中分段鎖的設(shè)計,而采用更為輕量級的CAS來協(xié)調(diào)并發(fā),效率更佳。關(guān)于java8中的ConcurrentHashMap的分析可以參考文章Java 8 ConcurrentHashMap源碼分析

雖然Striped64的設(shè)計類似于分段鎖算法,但是任然有其獨到之處,本文將分析Striped64的實現(xiàn)細(xì)節(jié),并且會分析基于Striped64的計數(shù)類LongAdder。Striped64的實現(xiàn)還是較為復(fù)雜的,本文會盡量分析,對于沒有充分了解的內(nèi)容,或者分析有誤的內(nèi)容,會在未來不斷修改補充。

下面首先展示了Striped64中的Cell類:

Cell類中僅有一個保存計數(shù)的變量value,并且為該變量提供了CAS操作方法,Cell類的實現(xiàn)雖然看起來很簡單,但是它的作用是非常大的,它是Striped64實現(xiàn)分散計數(shù)的最為基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),當(dāng)然為了達到并發(fā)環(huán)境下的線程安全以及高效,Striped64做了很多努力。Striped64中有兩個提供計數(shù)的api方法,分別為longAccumulate和doubleAccumulate,兩者的實現(xiàn)思路是一致的,只是前者對long類型計數(shù),而后者對double類型計數(shù),本文只分析前者的實現(xiàn),下面是longAccumulate方法的代碼:


final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        if ((h = getProbe()) == 0) { //獲取當(dāng)前線程的probe值,如果為0,則需要初始化該線程的probe值
            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;
            if ((as = cells) != null && (n = as.length) > 0) { //獲取cell數(shù)組
                if ((a = as[(n - 1) & h]) == null) { // 通過(hashCode & (length - 1))這種算法來實現(xiàn)取模
                    if (cellsBusy == 0) {       // 如果當(dāng)前位置為null說明需要初始化
                        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;
                } 
                //運行到此說明cell的對應(yīng)位置上已經(jīng)有想相應(yīng)的Cell了,不需要初始化了
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                    
                //嘗試去修改a上的計數(shù),a為Cell數(shù)組中index位置上的cell
                else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                             fn.applyAsLong(v, x))))
                    break;
                    
                //cell數(shù)組最大為cpu的數(shù)量,cells != as表面cells數(shù)組已經(jīng)被更新了    
                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]; //Cell數(shù)組擴容,每次擴容為原來的兩倍
                            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);
            }
            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;
            }
            else if (casBase(v = base, ((fn == null) ? v + x :
                                        fn.applyAsLong(v, x))))
                break;                          // Fall back on using base
        }
    }

僅從代碼量上就可以意識到longAccumulate的實現(xiàn)時異常復(fù)雜的,下面來梳理一下該方法的運行邏輯:

  • longAccumulate會根據(jù)當(dāng)前線程來計算一個哈希值,然后根據(jù)算法(hashCode & (length - 1))來達到取模的效果以定位到該線程被分散到的Cell數(shù)組中的位置
  • 如果Cell數(shù)組還沒有被創(chuàng)建,那么就去獲取cellBusy這個共享變量(相當(dāng)于鎖,但是更為輕量級),如果獲取成功,則初始化Cell數(shù)組,初始容量為2,初始化完成之后將x保證成一個Cell,哈希計算之后分散到相應(yīng)的index上。如果獲取cellBusy失敗,那么會試圖將x累計到base上,更新失敗會重新嘗試直到成功。
  • 如果Cell數(shù)組以及被初始化過了,那么就根據(jù)線程的哈希值分散到一個Cell數(shù)組元素上,獲取這個位置上的Cell并且賦值給變量a,這個a很重要,如果a為null,說明該位置還沒有被初始化,那么就初始化,當(dāng)然在初始化之前需要競爭cellBusy變量。
  • 如果Cell數(shù)組的大小已經(jīng)最大了(CPU的數(shù)量),那么就需要重新計算哈希,來重新分散當(dāng)前線程到另外一個Cell位置上再走一遍該方法的邏輯,否則就需要對Cell數(shù)組進行擴容,然后將原來的計數(shù)內(nèi)容遷移過去。這里面需要注意的是,因為Cell里面保存的是計數(shù)值,所以在擴容之后沒有必要做其他的處理,直接根據(jù)index將舊的Cell數(shù)組內(nèi)容直接復(fù)制到新的Cell數(shù)組中就可以了。

當(dāng)然,上面的流程是高度概括的,longAccumulate的實際分支還要更多,并且為了保證線程安全做的判斷更多。longAccumulate會根據(jù)不同的狀態(tài)來執(zhí)行不同的分支,比如在線程競爭非常激烈的時候,會通過對cells數(shù)組擴容或者從新計算哈希值來重新分散線程,這些做法的目的是將多個線程的計數(shù)請求分散到不同的cells的index上,其實這和java7中的ConcurrentHashMap的設(shè)計思路是完全一致的,但是java7中的ConcurrentHashMap實現(xiàn)在segment加鎖使用了比較重的synchronized,而Striped64使用了java中較為底層的Unsafe類的CAS操作來進行并發(fā)操作,這種方式更為輕量級,因為它會不停的嘗試,失敗會返回,而加鎖的方式會阻塞線程,線程需要被喚醒,這涉及到了線程的狀態(tài)的改變,需要上下文切換,所以是比較重量級的。

Unsafe

在這里添加一點關(guān)于java中底層操作的類Unsafe類的使用方法,首先看下面的代碼:

Unsafe需要關(guān)注的是Field的offset,然后在CAS的時候需要oldValue和expectValue以及newValue,它會在比較了oldValue == exceptValue的時候?qū)ldValue設(shè)置為newValue,否則不會改變。這也是CAS的定義,(compare And set)下面的代碼展示了CAS操作的示例:


UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val)

this是需要改變的對象,valueOffset為需要修改的Field在該對象中的offset,這個值的獲取可以參考上面展示的
圖片,cmp為exceptValue,也就是我們希望他的舊值為cmp值,如果相等,則將該Field設(shè)置為val,否則別修改。

LongAdder實現(xiàn)細(xì)節(jié)

上文中分析了Striped64的實現(xiàn)細(xì)節(jié),下面來分析一下LongAdder的實現(xiàn)細(xì)節(jié),LongAdder的實現(xiàn)基于Striped64,理解了Striped64就很好理解LongAdder了。下面先來看一下LongAdder的add方法:

首先判斷cells是否為null,如果為null,則會嘗試將本次計數(shù)累計到base上,如果cells不為null,或者操作base失敗,那么就會通過哈希值來獲取當(dāng)前線程對應(yīng)的cells數(shù)組中的位置,獲取該位置上的cell,如果該cell不為null,那么就試圖將本次計數(shù)累計到該cell上,如果不成功,那么就需要借助Striped64類的longAccumulate方法來進行計數(shù)累計,關(guān)于longAccumulate的分析見上文。

當(dāng)我們想要獲得當(dāng)前的總計數(shù)的時候,需要調(diào)用sum方法來獲取,下面展示了該方法的細(xì)節(jié):

它需要累計base和Cell數(shù)組中的Cell中的計數(shù),base中的計數(shù)為線程競爭不是很激烈的時候累計的數(shù),而在線程競爭比較激烈的時候就會將計數(shù)的任務(wù)分散到Cell數(shù)組中,所以在sum方法里,需要合并兩處的計數(shù)值。

除了獲取總計數(shù),我們有時候想reset一下,下面的代碼展示了這種操作:


    public void reset() {
        Cell[] as = cells; Cell a;
        base = 0L;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    a.value = 0L;
            }
        }
    }

同樣注意點在于需要同時將base和Cell數(shù)組都reset。

Striped64在ConcurrentHashMap中的使用

Striped64的計數(shù)方法在java8的ConcurrentHashMap中也有使用,具體的實現(xiàn)細(xì)節(jié)可以參考addCount方法,下面來看一下ConcurrentHashMap的size方法的實現(xiàn)細(xì)節(jié):


    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

ConcurrentHashMap中的baseCount對應(yīng)著Striped64中的base變量,而counterCells則對應(yīng)著Striped64中的cells數(shù)組,他們的實現(xiàn)時一樣的,更為詳細(xì)的內(nèi)容可以參考java8中的ConcurrentHashMap實現(xiàn)。

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

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