ConcurrentHashMap源碼分析(JDK1.8)——擴(kuò)容

前言

在分析ConcurrentHashMap之前,希望大家先讀完HashMap的源碼命贴,因?yàn)镃oncurrentHashMap基本算法和HashMap是一致的,只是增加了并發(fā)控制而已,有了HashMap的基礎(chǔ)才能更好的理解ConcurrentHashMap,推薦大家先看看這兩篇文章:
HashMap的hash機(jī)制詳解
HashMap源碼分析

1. 重要成員

/**
     * 初始化和擴(kuò)容標(biāo)志供置,也是并發(fā)控制非常重要的一員,當(dāng)sizeCtl<0時(shí)
     * 表明當(dāng)前正在初始化或擴(kuò)容己肮,sizeCtl=-1正在初始化士袄,sizeCtl<-1說(shuō)明正在擴(kuò)容
     * 而且此時(shí)sizeCtl = -(1+正在擴(kuò)容的線程數(shù)量).  
     * 當(dāng)還未進(jìn)行初始化時(shí)sizeCtl為初始化容量大小,默認(rèn)16谎僻,
     */
    private transient volatile int sizeCtl;

    /**
     * 擴(kuò)容時(shí)使用
     */
    private transient volatile int transferIndex;


    /**
    *真正存儲(chǔ)數(shù)據(jù)的數(shù)組
    */
    transient volatile Node<K,V>[] table;

這里要注意,此處沒(méi)有了JDK1.7中的分段鎖的概念了寓辱,全部都是基于CAS的艘绍。

2. 并發(fā)基礎(chǔ)——CAS

整個(gè)ConcurrentHashMap完全沒(méi)有方法級(jí)別的鎖,到底是什么機(jī)制來(lái)保證并發(fā)的呢秫筏?這里簡(jiǎn)單的介紹下诱鞠。
首先大家對(duì)樂(lè)觀鎖和悲觀鎖要有個(gè)大致理解:

  • 悲觀鎖 :悲觀鎖認(rèn)為競(jìng)爭(zhēng)一定會(huì)發(fā)生,所以不管如何都會(huì)鎖住資源这敬,不允許其他線程進(jìn)入航夺,sychorinized關(guān)鍵字就是標(biāo)準(zhǔn)的悲觀鎖
  • 樂(lè)觀鎖: 所謂的樂(lè)觀鎖就是認(rèn)為競(jìng)爭(zhēng)不一定會(huì)發(fā)生,比如有個(gè)變量A=3崔涂,我希望將它變成A=4,那么可以先比較 如果A=3,那么說(shuō)明沒(méi)有其他線程競(jìng)爭(zhēng)修改這個(gè)變量阳掐,我可以直接設(shè)置A=4, 這個(gè)比較和設(shè)置過(guò)程在硬件上是原子級(jí)別的,如果比較時(shí)發(fā)現(xiàn)A!=3,說(shuō)明有其他線程修改了冷蚂,這個(gè)情況會(huì)被返回缭保,調(diào)用者可以針對(duì)這個(gè)情況特殊處理。
    我們看下ConcurrentHashMap是如何將一個(gè)Node節(jié)點(diǎn)放到數(shù)組table的一個(gè)位置上的:
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

這里重點(diǎn)關(guān)注傳入的c和v蝙茶,整個(gè)compareAndSwapObject就是先比較tab數(shù)組上某個(gè)位置(通過(guò)內(nèi)存偏移量算出來(lái)的) 上的節(jié)點(diǎn)是不是 c艺骂,如果是就認(rèn)為沒(méi)有競(jìng)爭(zhēng),直接將該位置設(shè)置為 v隆夯,否則返回false钳恕。一般都是通過(guò)一個(gè)死循環(huán)來(lái)調(diào)用這個(gè)方法的,比如:

for (Node<K,V>[] tab = table;;) {  
       if (casTabAt(tab, i, null, r)) {
             //修改成功蹄衷,會(huì)繼續(xù)執(zhí)行其他業(yè)務(wù)
            break;            
       }
       //修改失敗忧额,會(huì)死循環(huán)下次重試
}

這種機(jī)制就是保證ConcurrentHashMap高效并發(fā)的基礎(chǔ)了。

由于ConcurrentHashMap處理hash沖突以及hash算法都是一樣的宦芦,所以這里一些基本功能不再分析宙址,我們重點(diǎn)分析一些由于并發(fā)導(dǎo)致的和HashMap區(qū)別較大的方法。

3. 擴(kuò)容

ConcurrentHashMap的擴(kuò)容是支持多線程并發(fā)擴(kuò)容的调卑,所以擴(kuò)容效率很高抡砂,在看源碼之前大咱,我們先大致看下并發(fā)擴(kuò)容的思想,擴(kuò)容的核心就在于將舊的table數(shù)組中的數(shù)據(jù)遷移到新的數(shù)組中來(lái)注益。我們先看張圖:


并發(fā)擴(kuò)容.png

之所以能并發(fā)擴(kuò)容就在于這里碴巾,將現(xiàn)有的數(shù)據(jù)分成了幾部分,每個(gè)線程領(lǐng)一個(gè)自己的部分 丑搔,線程領(lǐng)到了自己的部分后如何復(fù)制到新的數(shù)組的呢厦瓢?


單個(gè)線程復(fù)制過(guò)程

對(duì)于擴(kuò)容的單個(gè)線程來(lái)說(shuō),每次復(fù)制都是從尾部開(kāi)始啤月,一個(gè)節(jié)點(diǎn)復(fù)制完畢后會(huì)在這個(gè)位置放置一個(gè)ForwardingNode節(jié)點(diǎn)煮仇,表明這個(gè)節(jié)點(diǎn)已經(jīng)處理過(guò)了。

有了上述基礎(chǔ)我們?cè)俳Y(jié)合代碼分析.

3.1 確定每個(gè)線程擴(kuò)容時(shí)負(fù)責(zé)的數(shù)組部分長(zhǎng)度

        int n = tab.length, stride;
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range

這里主要是根據(jù)cpu的數(shù)量來(lái)計(jì)算的谎仲,但是如果算出來(lái)小于16的話浙垫,stride=16,也就是說(shuō)一個(gè)線程處理的數(shù)量最少是16.

3.2 申請(qǐng)新空間

擴(kuò)容第一步就是申請(qǐng)新的table數(shù)組郑诺,這個(gè)和HashMap一樣夹姥,都是直接兩倍擴(kuò)容:

       if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }

3.2 遍歷自己負(fù)責(zé)的所有元素

        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)   //分支1
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {   //分支2
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt    // 分支3
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
             //省略一些具體實(shí)現(xiàn) 
        }

這里以一個(gè)長(zhǎng)度32的ConcurrentHashMap擴(kuò)容到64為例,記住辙诞,之前在申請(qǐng)空間時(shí):

        if (nextTab == null) {            // initiating
         //省略代碼
            nextTable = nextTab;
            transferIndex = n;
        }

這也就意味著辙售,一個(gè)線程剛開(kāi)始擴(kuò)容時(shí),transferIndex = 32, i=0,bound=0;,所以會(huì)進(jìn)入分支3飞涂,
這個(gè)時(shí)候會(huì)通過(guò)CAS操作將transferIndex賦值為16旦部,bound=16, i=31. 記住transferIndex,每個(gè)線程擴(kuò)容起始位置都是由它決定的,這個(gè)線程將他改成了16封拧,那么下個(gè)并發(fā)線程擴(kuò)容就會(huì)從16開(kāi)始了志鹃,從而做到每個(gè)線程負(fù)責(zé)自己的數(shù)據(jù)。最終大部分操作都會(huì)進(jìn)入分支1泽西,通過(guò)--i來(lái)遍歷該線程負(fù)責(zé)的部分?jǐn)?shù)組曹铃,從而拷貝數(shù)據(jù)。

3.3 遷移table數(shù)組中的單個(gè)元素捧杉。

3.3.1 該位置沒(méi)有數(shù)據(jù)

          else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);

這個(gè)時(shí)候無(wú)需拷貝陕见,只要將再這個(gè)位置放置一個(gè)ForwardingNode節(jié)點(diǎn)即可。

3.3.2 該位置已經(jīng)被拷貝過(guò)了

else if ((fh = f.hash) == MOVED)
        advance = true; // already processed

此處的MOVED為-1味抖,F(xiàn)orwardingNode的hash值都為-1评甜,說(shuō)明這個(gè)節(jié)點(diǎn)已經(jīng)被處理過(guò)了。所有數(shù)據(jù)拷貝完成后會(huì)重新遍歷一遍檢查仔涩,這個(gè)時(shí)候時(shí)會(huì)進(jìn)入這個(gè)分支忍坷。

3.3.3 該位置是一個(gè)鏈表

            synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
              //省略部分代碼
            }

這里的思想其實(shí)和HashMap一樣(不熟悉的可以先看看本文開(kāi)始推薦的兩篇文章),都是把鏈表拆成兩部分,一部分放在nextTab的i位置佩研,一部分放在nextTab的i+n位置柑肴。
注意,這里使用了synchronized鎖住了當(dāng)前節(jié)點(diǎn)旬薯,這也是一種沒(méi)辦法的事晰骑。但由于鎖住的只是一個(gè)節(jié)點(diǎn),并不會(huì)影響到其他擴(kuò)容線程绊序。

3.3.4 該位置是一個(gè)紅黑樹(shù)

                  else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }

紅黑樹(shù)道理也是一樣硕舆,也是拆分成兩部分,但這里會(huì)統(tǒng)計(jì)放在nextTab的i位置的數(shù)量和i+n位置的數(shù)量骤公,如果低于6會(huì)退化成鏈表抚官。

3.3.5 擴(kuò)容結(jié)束

          if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {  //分支1
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {  //分支2
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        //走到這里說(shuō)明所有的擴(kuò)容線程都結(jié)束了,也就是此次擴(kuò)容徹底結(jié)束阶捆。
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }

當(dāng)自己的部分?jǐn)?shù)組都copy完畢后耗式,通常會(huì)先進(jìn)入分支2,將finishing置為true趁猴,i=n,由于此時(shí)i=n,會(huì)重新遍歷一遍自己負(fù)責(zé)的部分?jǐn)?shù)組,確保每個(gè)節(jié)點(diǎn)都被復(fù)制了彪见,最后會(huì)進(jìn)入分支1儡司,將sizeCtl 置為下次擴(kuò)容的閾值,其實(shí)sizeCtl = n2-n/2 = 2n 0.75. 也就是新的容量乘以擴(kuò)容因子余指。至此捕犬,此次該線程擴(kuò)容結(jié)束。

4. 總結(jié)

總體而言酵镜,本文并沒(méi)有再重復(fù)介紹一些和HashMap中一樣的算法碉碉,比如具體的hash算法,比如如何判斷擴(kuò)容時(shí)怎樣將一個(gè)鏈表拆成兩部分淮韭,主要介紹了思路以及并發(fā)相關(guān)的垢粮,個(gè)人理解,如果有錯(cuò)誤懇請(qǐng)指正靠粪。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜡吧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子占键,更是在濱河造成了極大的恐慌昔善,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畔乙,死亡現(xiàn)場(chǎng)離奇詭異君仆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)返咱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钥庇,“玉大人,你說(shuō)我怎么就攤上這事洛姑∩香澹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵楞艾,是天一觀的道長(zhǎng)参咙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)硫眯,這世上最難降的妖魔是什么蕴侧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮两入,結(jié)果婚禮上净宵,老公的妹妹穿的比我還像新娘。我一直安慰自己裹纳,他們只是感情好择葡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著剃氧,像睡著了一般敏储。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上朋鞍,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天已添,我揣著相機(jī)與錄音,去河邊找鬼滥酥。 笑死更舞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坎吻。 我是一名探鬼主播缆蝉,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼禾怠!你這毒婦竟也來(lái)了返奉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吗氏,失蹤者是張志新(化名)和其女友劉穎芽偏,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體弦讽,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡污尉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年膀哲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片被碗。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡某宪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出锐朴,到底是詐尸還是另有隱情兴喂,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布焚志,位于F島的核電站衣迷,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏酱酬。R本人自食惡果不足惜壶谒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望膳沽。 院中可真熱鬧汗菜,春花似錦、人聲如沸挑社。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)痛阻。三九已至普碎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間录平,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工缀皱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斗这,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓啤斗,卻偏偏與公主長(zhǎng)得像表箭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钮莲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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