ConcurrentHashMap JDK1.8原理分析

1.7與1.8區(qū)別

1.7采用分段鎖的概念糕韧,如下圖所示葱跋,每段包含多個節(jié)點页滚,并且都是加悲觀鎖


1.7

1.8同樣是采用分段鎖的思想烁竭,只是這次將分段鎖的粒度降低到節(jié)點級別菲茬,并且采用了部分CAS樂觀鎖的操作,大大提升了并發(fā)性能
數(shù)據(jù)結構沿用了與它同時期的HashMap版本的思想,底層依然由數(shù)組+鏈表+紅黑樹的方式思想生均。
有一個最重要的不同點就是ConcurrentHashMap不允許key或value為null值


1.8

重點變量

    /**
     * 盛裝Node元素的數(shù)組 它的大小是2的整數(shù)次冪
     * Size is always a power of two. Accessed directly by iterators.
     */
    transient volatile Node<K,V>[] table;
        
        /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     hash表初始化或擴容時的一個控制位標識量听想。
     負數(shù)代表正在進行初始化或擴容操作
     -1代表正在初始化
     -N 表示有N-1個線程正在進行擴容操作
     正數(shù)或0代表hash表還沒有被初始化,這個數(shù)值表示初始化或下一次進行擴容的大小
     
     */
    private transient volatile int sizeCtl; 

Node

Node是ConcurrentHashMap最核心的內部類马胧,每個Node可以理解為數(shù)組中的一個節(jié)點

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;//帶有同步鎖的value
        volatile Node<K,V> next;//帶有同步鎖的next指針

get

因為Node的val值域是volatile的汉买,所以無需加鎖就可以得到節(jié)點的最新值

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //計算hash值
        int h = spread(key.hashCode());
        //根據(jù)hash值確定節(jié)點位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            //如果搜索到的節(jié)點key與傳入的key相同且不為null,直接返回這個節(jié)點  
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            //如果eh<0 說明這個節(jié)點在樹上 直接尋找
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
             //否則遍歷鏈表 找到對應的值并返回
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

put

簡潔說明:

  • 根據(jù)給定的key的hash值找到其在table中的位置index
  • 找到位置index后,根據(jù)以下情況進行存儲或者幫助擴容后存儲
  1. 如果當前正在擴容佩脊,則優(yōu)先幫助擴容
  2. 如果table[index]位置沒有元素蛙粘,則直接通過CAS存儲
  3. 如果table[i]存儲的是一個鏈表:如果鏈表不存在key則直接加入到鏈表尾部;如果存在key則更新其對應的value威彰;如果存入后鏈表元素>8出牧,還需要將鏈表轉換為紅黑樹
  4. 如果table[i]存儲的是一個紅黑樹,則按照紅黑樹方式插入

其中3跟4需要synchronized對頭節(jié)點進行加鎖

public V put(K key, V value) {  
        return putVal(key, value, false);  
    }  
  
    /** Implementation for put and putIfAbsent */  
    final V putVal(K key, V value, boolean onlyIfAbsent) {  
            //不允許 key或value為null  
        if (key == null || value == null) throw new NullPointerException();  
        //計算hash值  
        int hash = spread(key.hashCode());  
        int binCount = 0;  
        //死循環(huán) 何時插入成功 何時跳出  
        for (Node<K,V>[] tab = table;;) {  
            Node<K,V> f; int n, i, fh;  
            //如果table為空的話歇盼,初始化table  
            if (tab == null || (n = tab.length) == 0)  
                tab = initTable();  
            //根據(jù)hash值計算出在table里面的位置   
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {  
                //如果這個位置沒有值 舔痕,直接放進去,不需要加鎖  
                if (casTabAt(tab, i, null,  
                             new Node<K,V>(hash, key, value, null)))  
                    break;                   // no lock when adding to empty bin  
            }  
            //當遇到表連接點時豹缀,需要進行整合表的操作  
            else if ((fh = f.hash) == MOVED)  
                tab = helpTransfer(tab, f);  
            else {  
                V oldVal = null;  
                //結點上鎖  這里的結點可以理解為hash值相同組成的鏈表的頭結點  
                synchronized (f) {  
                    if (tabAt(tab, i) == f) {  
                        //fh〉0 說明這個節(jié)點是一個鏈表的節(jié)點 不是樹的節(jié)點  
                        if (fh >= 0) {  
                            binCount = 1;  
                            //在這里遍歷鏈表所有的結點  
                            for (Node<K,V> e = f;; ++binCount) {  
                                K ek;  
                                //如果hash值和key值相同  則修改對應結點的value值  
                                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;  
                                //如果遍歷到了最后一個結點伯复,那么就證明新的節(jié)點需要插入 就把它插入在鏈表尾部  
                                if ((e = e.next) == null) {  
                                    pred.next = new Node<K,V>(hash, key,  
                                                              value, null);  
                                    break;  
                                }  
                            }  
                        }  
                        //如果這個節(jié)點是樹節(jié)點,就按照樹的方式插入值  
                        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;  
                            }  
                        }  
                    }  
                }  
                if (binCount != 0) {  
                    //如果鏈表長度已經(jīng)達到臨界值8 就需要把鏈表轉換為樹結構  
                    if (binCount >= TREEIFY_THRESHOLD)  
                        treeifyBin(tab, i);  
                    if (oldVal != null)  
                        return oldVal;  
                    break;  
                }  
            }  
        }  
        //將當前ConcurrentHashMap的元素數(shù)量+1  
        addCount(1L, binCount);  
        return null;  
    }  
      

size

ConcurrentHashMap 中鍵值對的個數(shù)通過求 baseCount 與 counterCells 非空元素的和得到

    /**
     * Base counter value, used mainly when there is no contention,
     * but also as a fallback during table initialization
     * races. Updated via CAS.
     * 當沒有爭用時邢笙,使用這個變量計數(shù)啸如。
     */
    private transient volatile long baseCount;

    /**
     * Table of counter cells. When non-null, size is a power of 2.
     */
    private transient volatile CounterCell[] counterCells;

static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

一個 volatile 的變量,在 addCount 方法中會使用它氮惯,而 addCount 方法在 put 結束后會調用叮雳。在 addCount 方法中,會對這個變量做 CAS 加法
但是如果并發(fā)導致 CAS 失敗了妇汗,使用 counterCells
如果使用 counterCells CAS 失敗了帘不,在 fullAddCount 方法中,會繼續(xù)死循環(huán)操作杨箭,直到成功

這種方式目的是降低更新size時的沖突厌均,提升性能

擴容

整個擴容操作分為兩個部分
第一部分是構建一個nextTable,它的容量是原來的兩倍,這個操作是單線程完成的告唆。這個單線程的保證是通過RESIZE_STAMP_SHIFT這個常量經(jīng)過一次運算來保證的
第二個部分就是將原來table中的元素復制到nextTable中棺弊,這里允許多線程進行操作。

先來看一下單線程是如何完成的:
它的大體思想就是遍歷擒悬、復制的過程模她。首先根據(jù)運算得到需要遍歷的次數(shù)i,然后利用tabAt方法獲得i位置的元素:
如果這個位置為空懂牧,就在原table中的i位置放入forwardNode節(jié)點侈净,這個也是觸發(fā)并發(fā)擴容的關鍵點尊勿;
如果這個位置是Node節(jié)點(fh>=0),如果它是一個鏈表的頭節(jié)點畜侦,就構造一個反序鏈表元扔,把他們分別放在nextTable的i和i+n的位置上,然后在原table中的i位置放入forwardNode節(jié)點
如果這個位置是TreeBin節(jié)點(fh<0)旋膳,也做一個反序處理澎语,并且判斷是否需要untreeify,把處理的結果分別放在nextTable的i和i+n的位置上验懊,然后在原table中的i位置放入forwardNode節(jié)點
遍歷過所有的節(jié)點以后就完成了復制工作擅羞,這時讓nextTable作為新的table,并且更新sizeCtl為新容量的0.75倍 义图,完成擴容减俏。

再看一下多線程是如何完成的:
多線程遍歷節(jié)點,處理了一個節(jié)點碱工,就把對應點的值set為forward娃承,另一個線程看到ForwardingNode節(jié)點,就向后遍歷怕篷。

image.png

Key和Value不允許null值

ConcurrentHashmap和Hashtable都是支持并發(fā)的草慧,這樣會有一個問題,當你通過get(k)獲取對應的value時匙头,如果獲取到的是null時,你無法判斷仔雷,它是put(k,v)的時候value為null蹂析,還是這個key從來沒有做過映射。
HashMap是非并發(fā)的碟婆,可以通過contains(key)來做這個判斷电抚。
而ConcurrentHashMap在調用m.containsKey(key)和m.get(key),這兩個方法都是沒有加鎖的竖共,調用時候m可能被其他線程改變了蝙叛。
假如一個線程m.containsKey(k)為真,在還沒執(zhí)行m.get(k)的時候公给,k被另外一個線程給刪除了借帘,那么m.get(k)會返回null。如果允許null值的話淌铐,就會錯誤的判斷為k還存在肺然;因此不允許null值的話就可以正常的表示出當前的k是不存在的。所以在ConcurrentHashMap不應該有如下的寫法腿准,Key和Value不允許null值际起。
其實Value不允許null值就可以,Key為null似乎沒什么影響,作者一起排除null我也不知道什么原因街望。

if (m.containsKey(k)) {
   return m.get(k);
} else {
   throw new KeyNotPresentException();
}

總結

  • Node級別分段鎖
  • 讀不加鎖
  • 寫不一定需要加鎖
  • 可多線程擴容
  • size計算方式特殊
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末校翔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子灾前,更是在濱河造成了極大的恐慌防症,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件豫柬,死亡現(xiàn)場離奇詭異告希,居然都是意外死亡,警方通過查閱死者的電腦和手機烧给,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門燕偶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人础嫡,你說我怎么就攤上這事指么。” “怎么了榴鼎?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵伯诬,是天一觀的道長。 經(jīng)常有香客問我巫财,道長盗似,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任平项,我火速辦了婚禮赫舒,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好应媚,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缺猛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪椭符。 梳的紋絲不亂的頭發(fā)上荔燎,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音销钝,去河邊找鬼湖雹。 笑死,一個胖子當著我的面吹牛曙搬,可吹牛的內容都是我干的摔吏。 我是一名探鬼主播鸽嫂,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼征讲!你這毒婦竟也來了据某?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤诗箍,失蹤者是張志新(化名)和其女友劉穎癣籽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滤祖,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡筷狼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了匠童。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片埂材。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖汤求,靈堂內的尸體忽然破棺而出俏险,到底是詐尸還是另有隱情,我是刑警寧澤扬绪,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布竖独,位于F島的核電站,受9級特大地震影響挤牛,放射性物質發(fā)生泄漏莹痢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一墓赴、第九天 我趴在偏房一處隱蔽的房頂上張望竞膳。 院中可真熱鬧,春花似錦竣蹦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滔吠,卻和暖如春纲菌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疮绷。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工翰舌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冬骚。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓椅贱,卻偏偏與公主長得像懂算,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子庇麦,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內容