并發(fā)編程之 ConcurrentHashMap(JDK 1.8) putVal 源碼分析

前言

我們之前分析了Hash的源碼,主要是 put 方法宦棺。同時瓣距,我們知道,HashMap 在并發(fā)的時候是不安全的代咸,為什么呢蹈丸?因為當多個線程對 Map 進行擴容會導致鏈表成環(huán)。不單單是這個問題呐芥,當多個線程相同一個槽中插入數(shù)據(jù)逻杖,也是不安全的。而在這之后思瘟,我們學習了并發(fā)編程荸百,而并發(fā)編程中有一個重要的東西,就是JDK 自帶的并發(fā)容器滨攻,提供了線程安全的特性且比同步容器性能好出很多够话。一個典型的代表就是 ConcurrentHashMap,對光绕,又是 HashMap 女嘲,但是這個 Map 是線程安全的,那么同樣的诞帐,我們今天就看看該類的 put 方法是如何實現(xiàn)線程安全的欣尼。

源碼加注釋分析 putVal 方法

 /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 死循環(huán)執(zhí)行
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                // 初始化
                tab = initTable();
            // 獲取對應下標節(jié)點,如果是kong停蕉,直接插入
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // CAS 進行插入
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果 hash 沖突了愕鼓,且 hash 值為 -1钙态,說明是 ForwardingNode 對象(這是一個占位符對象,保存了擴容后的容器)
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 如果 hash 沖突了拒啰,且 hash 值不為 -1
            else {
                V oldVal = null;
                // 同步 f 節(jié)點驯绎,防止增加鏈表的時候?qū)е骆湵沓森h(huán)
                synchronized (f) {
                    // 如果對應的下標位置 的節(jié)點沒有改變
                    if (tabAt(tab, i) == f) {
                        // 并且 f 節(jié)點的hash 值 不是大于0
                        if (fh >= 0) {
                            // 鏈表初始長度
                            binCount = 1;
                            // 死循環(huán),直到將值添加到鏈表尾部谋旦,并計算鏈表的長度
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 如果 f 節(jié)點的 hasj 小于0 并且f 是 樹類型
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 鏈表長度大于等于8時剩失,將該節(jié)點改成紅黑樹樹
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 判斷是否需要擴容
        addCount(1L, binCount);
        return null;
    }

樓主在代碼中寫了很多注釋,但是還是說一下步驟(該方法和HashMap 的高度相似册着,但是多了很多同步操作)拴孤。

  1. 校驗key value 值,都不能是null甲捏。這點和 HashMap 不同演熟。
  2. 得到 key 的 hash 值。
  3. 死循環(huán)并更新 tab 變量的值司顿。
  4. 如果容器沒有初始化芒粹,則初始化。調(diào)用 initTable 方法大溜。該方法通過一個變量 + CAS 來控制并發(fā)化漆。稍后我們分析源碼。
  5. 根據(jù) hash 值找到數(shù)組下標钦奋,如果對應的位置為空座云,就創(chuàng)建一個 Node 對象用CAS方式添加到容器。并跳出循環(huán)付材。
  6. 如果 hash 沖突朦拖,也就是對應的位置不為 null,則判斷該槽是否被擴容了(-1 表示被擴容了)厌衔,如果被擴容了璧帝,返回新的數(shù)組。
  7. 如果 hash 沖突 且 hash 值不是 -1富寿,表示沒有被擴容睬隶。則進行鏈表操作或者紅黑樹操作,注意作喘,這里的 f 頭節(jié)點被鎖住了理疙,保證了同時只有一個線程修改鏈表晕城。防止出現(xiàn)鏈表成環(huán)泞坦。
  8. 和 HashMap 一樣,如果鏈表樹超過8砖顷,則修改鏈表為紅黑樹贰锁。
  9. 將數(shù)組加1(CAS方式)赃梧,如果需要擴容,則調(diào)用 transfer 方法(非常復雜豌熄,以后再詳解)進行移動和重新散列授嘀,該方法中,如果是槽中只有單個節(jié)點锣险,則使用CAS直接插入蹄皱,如果不是,則使用 synchronized 進行同步芯肤,防止并發(fā)成環(huán)粪小。

這里說一說 initTable 方法:

    /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            // 小于0說明被其他線程改了
            if ((sc = sizeCtl) < 0)
                // 自旋等待
                Thread.yield(); // lost initialization race; just spin
            // CAS 修改 sizeCtl 的值為-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        // sc 在初始化的時候用戶可能會自定義静檬,如果沒有自定義,則是默認的
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        // 創(chuàng)建數(shù)組
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // sizeCtl 計算后作為擴容的閥值
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }


該方法為了在并發(fā)環(huán)境下的安全,加入了一個 sizeCtl 變量來進行判斷绳瘟,只有當一個線程通過CAS修改該變量成功后(默認為0,改成 -1)毡琉,該線程才能初始化數(shù)組蕾久。保證了初始化數(shù)組時的安全性。

總結(jié)

ConcurrentHashMap 是并發(fā)大師 Doug Lea 的杰作歌豺,可以說鬼斧神工推穷,總的來說,使用了 CAS 加 synchronized 來保證了 put 操作并發(fā)時的危險(特別是鏈表)世曾,相比 同步容器 hashTable 來說缨恒,如果容器大小是16,并發(fā)的性能是他的16倍轮听,注意骗露,讀的時候是沒有鎖的,完全并發(fā)血巍,而 HashTable 在 get 方法上直接加上了 synchronized 關(guān)鍵字萧锉,性能差距不言而喻。

當然述寡,樓主這篇文章可能之寫到了 ConcurrentHashMap 的皮毛柿隙,關(guān)于如何擴容,樓主沒有詳細介紹鲫凶,而樓主在閱讀源碼的收獲也很多禀崖,發(fā)現(xiàn)了很多有趣的東西,比如 ThreadLocalRandom 類在 addCount 方法中的應用螟炫,大家可以看看該類波附,非常的實用。

注意:這篇文章僅僅是 ConcurrentHashMap 的開頭,關(guān)于 ConcurrentHashMap 里面的精華太多掸屡,值得我們好好學習封寞。

good luck !=霾啤1肪俊!盏求!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抖锥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子碎罚,更是在濱河造成了極大的恐慌宁改,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件魂莫,死亡現(xiàn)場離奇詭異还蹲,居然都是意外死亡,警方通過查閱死者的電腦和手機耙考,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門谜喊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人倦始,你說我怎么就攤上這事斗遏。” “怎么了鞋邑?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵诵次,是天一觀的道長。 經(jīng)常有香客問我枚碗,道長逾一,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任肮雨,我火速辦了婚禮遵堵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘怨规。我一直安慰自己陌宿,他們只是感情好,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布波丰。 她就那樣靜靜地躺著壳坪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掰烟。 梳的紋絲不亂的頭發(fā)上爽蝴,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天扩灯,我揣著相機與錄音,去河邊找鬼霜瘪。 笑死,一個胖子當著我的面吹牛惧磺,可吹牛的內(nèi)容都是我干的颖对。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼磨隘,長吁一口氣:“原來是場噩夢啊……” “哼缤底!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起番捂,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤个唧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后设预,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體徙歼,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年鳖枕,在試婚紗的時候發(fā)現(xiàn)自己被綠了魄梯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡宾符,死狀恐怖酿秸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情魏烫,我是刑警寧澤辣苏,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站哄褒,受9級特大地震影響稀蟋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呐赡,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一糊治、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧罚舱,春花似錦井辜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至包个,卻和暖如春刷允,著一層夾襖步出監(jiān)牢的瞬間冤留,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工树灶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留纤怒,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓天通,卻偏偏與公主長得像泊窘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子像寒,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

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

  • 1.ConcurrentHashmap簡介 在使用HashMap時在多線程情況下擴容會出現(xiàn)CPU接近100%的情況...
    你聽___閱讀 4,633評論 2 20
  • 相關(guān)文章Java并發(fā)編程(一)線程定義烘豹、狀態(tài)和屬性 Java并發(fā)編程(二)同步Java并發(fā)編程(三)volatil...
    劉望舒閱讀 2,626評論 0 22
  • Java8張圖 11、字符串不變性 12诺祸、equals()方法携悯、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,696評論 0 11
  • 高黎貢不為人知的博物館 幾年前偶然知道了她筷笨,就想著要是有一天能去看看就好了憔鬼。 幸運的是讓我遇到了愿意陪著我任性的小...
    焦糖布丁BG7閱讀 1,242評論 4 7
  • 我要去你的小時候 撿起你掉在地上的小花 送給你五顏六色的蝴蝶結(jié) 摸摸你凌亂的頭發(fā) 我要去你的小時候 揉揉你紅撲撲的...
    YToxi閱讀 360評論 0 1