ConcurrentHashMap 的實(shí)現(xiàn)原理

我們了解到關(guān)于 HashMap 和 Hashtable 這兩種集合谨娜。其中 HashMap 是非線程安全的渠旁,當(dāng)我們只有一個(gè)線程在使用 HashMap 的時(shí)候竭翠,自然不會(huì)有問題脑蠕,但如果涉及到多個(gè)線程酌泰,并且有讀有寫的過程中媒佣,HashMap 就不能滿足我們的需要了(fail-fast)。在不考慮性能問題的時(shí)候陵刹,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap)默伍,這兩種方式基本都是對整個(gè) hash 表結(jié)構(gòu)做鎖定操作的,這樣在鎖表的期間衰琐,別的線程就需要等待了也糊,無疑性能不高。
所以我們學(xué)習(xí)一個(gè) util.concurrent 包的重要成員羡宙,ConcurrentHashMap狸剃。
ConcurrentHashMap 的實(shí)現(xiàn)是依賴于 Java 內(nèi)存模型,所以我們在了解 ConcurrentHashMap 的前提是必須了解Java 內(nèi)存模型狗热。但 Java 內(nèi)存模型并不是本文的重點(diǎn)捕捂,所以我假設(shè)讀者已經(jīng)對 Java 內(nèi)存模型有所了解。
ConcurrentHashMap 分析
ConcurrentHashMap 的結(jié)構(gòu)是比較復(fù)雜的斗搞,都深究去本質(zhì),其實(shí)也就是數(shù)組和鏈表而已慷妙。我們由淺入深慢慢的分析其結(jié)構(gòu)僻焚。
先簡單分析一下,ConcurrentHashMap 的成員變量中膝擂,包含了一個(gè) Segment 的數(shù)組(final Segment<K,V>[] segments;
)虑啤,而 Segment 是 ConcurrentHashMap 的內(nèi)部類,然后在 Segment 這個(gè)類中架馋,包含了一個(gè) HashEntry 的數(shù)組(transient volatile HashEntry<K,V>[] table;
)狞山。而 HashEntry 也是 ConcurrentHashMap 的內(nèi)部類。HashEntry 中叉寂,包含了 key 和 value 以及 next 指針(類似于 HashMap 中 Entry)萍启,所以 HashEntry 可以構(gòu)成一個(gè)鏈表。
所以通俗的講屏鳍,ConcurrentHashMap 數(shù)據(jù)結(jié)構(gòu)為一個(gè) Segment 數(shù)組勘纯,Segment 的數(shù)據(jù)結(jié)構(gòu)為 HashEntry 的數(shù)組,而 HashEntry 存的是我們的鍵值對钓瞭,可以構(gòu)成鏈表驳遵。
首先,我們看一下 HashEntry 類山涡。
HashEntry
HashEntry 用來封裝散列映射表中的鍵值對堤结。在 HashEntry 類中唆迁,key,hash 和 next 域都被聲明為 final 型竞穷,value 域被聲明為 volatile 型唐责。其類的定義為:

static final class HashEntry<K,V> { 
    final int hash; 
    final K key; 
    volatile V value;
    volatile HashEntry<K,V> next; 
    HashEntry(int hash, K key, V value, HashEntry<K,V> next) { 
        this.hash = hash; 
        this.key = key; 
        this.value = value; 
        this.next = next; 
    } ... ...
}

HashEntry 的學(xué)習(xí)可以類比著 HashMap 中的 Entry。我們的存儲(chǔ)鍵值對的過程中来庭,散列的時(shí)候如果發(fā)生“碰撞”妒蔚,將采用“分離鏈表法”來處理碰撞:把碰撞的 HashEntry 對象鏈接成一個(gè)鏈表。
如下圖月弛,我們在一個(gè)空桶中插入 A肴盏、B、C 兩個(gè) HashEntry 對象后的結(jié)構(gòu)圖(其實(shí)應(yīng)該為鍵值對帽衙,在這進(jìn)行了簡化以方便更容易理解):


圖1

Segment

Segment 的類定義為
static final class Segment<K,V> extends ReentrantLock implements Serializable

菜皂。其繼承于 ReentrantLock 類,從而使得 Segment 對象可以充當(dāng)鎖的角色厉萝。Segment 中包含HashEntry 的數(shù)組恍飘,其可以守護(hù)其包含的若干個(gè)桶(HashEntry的數(shù)組)。Segment 在某些意義上有點(diǎn)類似于 HashMap了谴垫,都是包含了一個(gè)數(shù)組章母,而數(shù)組中的元素可以是一個(gè)鏈表。
table:table 是由 HashEntry 對象組成的數(shù)組如果散列時(shí)發(fā)生碰撞翩剪,碰撞的 HashEntry 對象就以鏈表的形式鏈接成一個(gè)鏈表table數(shù)組的數(shù)組成員代表散列映射表的一個(gè)桶每個(gè) table 守護(hù)整個(gè) ConcurrentHashMap 包含桶總數(shù)的一部分如果并發(fā)級別為 16乳怎,table 則守護(hù) ConcurrentHashMap 包含的桶總數(shù)的 1/16。
count 變量是計(jì)算器前弯,表示每個(gè) Segment 對象管理的 table 數(shù)組(若干個(gè) HashEntry 的鏈表)包含的HashEntry 對象的個(gè)數(shù)蚪缀。之所以在每個(gè)Segment對象中包含一個(gè) count 計(jì)數(shù)器,而不在 ConcurrentHashMap 中使用全局的計(jì)數(shù)器恕出,是為了避免出現(xiàn)“熱點(diǎn)域”而影響并發(fā)性询枚。

/** 
  * Segments are specialized versions of hash tables. This  
  * subclasses from ReentrantLock opportunistically, just to 
  * simplify some locking and avoid separate construction. 
  */ 
static final class Segment<K,V> extends ReentrantLock implements Serializable { 
   /** 
     * The per-segment table. Elements are accessed via 
     * entryAt/setEntryAt providing volatile semantics. 
     */ 
    transient volatile HashEntry<K,V>[] table; 
    /** 
      * The number of elements. Accessed only either within locks 
      * or among other volatile reads that maintain visibility. 
      */ 
     transient int count; transient int modCount; 
     /** 
       * 裝載因子 
       */ 
     final float loadFactor; 
}

我們通過下圖來展示一下插入 ABC 三個(gè)節(jié)點(diǎn)后,Segment 的示意圖:


圖2

其實(shí)從我個(gè)人角度來說浙巫,Segment結(jié)構(gòu)是與HashMap很像的金蜀。
ConcurrentHashMap
ConcurrentHashMap 的結(jié)構(gòu)中包含的 Segment 的數(shù)組,在默認(rèn)的并發(fā)級別會(huì)創(chuàng)建包含 16 個(gè) Segment 對象的數(shù)組的畴。通過我們上面的知識(shí)才沧,我們知道每個(gè) Segment 又包含若干個(gè)散列表的桶淮椰,每個(gè)桶是由 HashEntry 鏈接起來的一個(gè)鏈表。如果 key 能夠均勻散列,每個(gè) Segment 大約守護(hù)整個(gè)散列表桶總數(shù)的 1/16漆弄。
下面我們還有通過一個(gè)圖來演示一下 ConcurrentHashMap 的結(jié)構(gòu):


圖3

并發(fā)寫操作
在 ConcurrentHashMap 中蔓罚,當(dāng)執(zhí)行 put 方法的時(shí)候眉踱,會(huì)需要加鎖來完成。我們通過代碼來解釋一下具體過程:當(dāng)我們 new 一個(gè) ConcurrentHashMap 對象抱慌,并且執(zhí)行put操作的時(shí)候,首先會(huì)執(zhí)行 ConcurrentHashMap 類中的 put 方法眨猎,該方法源碼為:
/** 
  * Maps the specified key to the specified value in this table. 
  * Neither the key nor the value can be null. 
  * 
  * <p> The value can be retrieved by calling the <tt>get</tt> method 
  * with a key that is equal to the original key. 
  * 
  * @param key key with which the specified value is to be associated 
  * @param value value to be associated with the specified key 
  * @return the previous value associated with <tt>key</tt>, or 
  * <tt>null</tt> if there was no mapping for <tt>key</tt> 
  * @throws NullPointerException if the specified key or value is null 
   */ 
@SuppressWarnings("unchecked") 
public V put(K key, V value) { 
    Segment<K,V> s; 
    if (value == null) 
        throw new NullPointerException(); 
    int hash = hash(key); 
    int j = (hash >>> segmentShift) & segmentMask; 
    if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; 
        recheck (segments, (j << SSHIFT) + SBASE)) == null)// in ensureSegment 
        s = ensureSegment(j); 
    return s.put(key, hash, value, false); 
}

我們通過注釋可以了解到抑进,ConcurrentHashMap 不允許空值。該方法首先有一個(gè) Segment 的引用 s睡陪,然后會(huì)通過 hash() 方法對 key 進(jìn)行計(jì)算寺渗,得到哈希值;繼而通過調(diào)用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進(jìn)行存儲(chǔ)操作兰迫。該方法源碼為:

final V put(K key, int hash, V value, boolean onlyIfAbsent) { 
    //加鎖信殊,這里是鎖定的Segment而不是整個(gè)  
    ConcurrentHashMap HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
    V oldValue; try { HashEntry<K,V>[] tab = table; 
    //得到hash對應(yīng)的table中的索引index 
    int index = (tab.length - 1) & hash; 
   //找到hash對應(yīng)的是具體的哪個(gè)桶,也就是哪個(gè)HashEntry鏈表 
    HashEntry<K,V> first = entryAt(tab, index); 
    for (HashEntry<K,V> e = first;;) { 
        if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { 
           oldValue = e.value; 
          if (!onlyIfAbsent) { 
            e.value = value; ++modCount; 
          } 
           break; 
          } 
         e = e.next; 
       } else { 
           if (node != null) 
                 node.setNext(first); 
           else 
                 node = new HashEntry<K,V>(hash, key, value, first); 
                 int c = count + 1; 
                 if (c > threshold && tab.length < MAXIMUM_CAPACITY) 
                     rehash(node); 
                  else setEntryAt(tab, index, node);   
              ++modCount; 
                count = c; 
               oldValue = null; 
               break; 
             } 
            }
          } finally { 
              //解鎖 unlock(); 
           }
           return oldValue;
  }

關(guān)于該方法的某些關(guān)鍵步驟汁果,在源碼上加上了注釋涡拘。
需要注意的是:加鎖操作是針對的 hash 值對應(yīng)的某個(gè) Segment,而不是整個(gè) ConcurrentHashMap据德。因?yàn)?put 操作只是在這個(gè) Segment 中完成鳄乏,所以并不需要對整個(gè) ConcurrentHashMap 加鎖。所以棘利,此時(shí)橱野,其他的線程也可以對另外的 Segment 進(jìn)行 put 操作,因?yàn)殡m然該 Segment 被鎖住了善玫,但其他的 Segment 并沒有加鎖水援。同時(shí),讀線程并不會(huì)因?yàn)楸揪€程的加鎖而阻塞蝌焚。
正是因?yàn)槠鋬?nèi)部的結(jié)構(gòu)以及機(jī)制,所以 ConcurrentHashMap 在并發(fā)訪問的性能上要比Hashtable和同步包裝之后的HashMap的性能提高很多誓斥。在理想狀態(tài)下只洒,ConcurrentHashMap 可以支持 16 個(gè)線程執(zhí)行并發(fā)寫操作(如果并發(fā)級別設(shè)置為 16),及任意數(shù)量線程的讀操作劳坑。
總結(jié)
在實(shí)際的應(yīng)用中毕谴,散列表一般的應(yīng)用場景是:除了少數(shù)插入操作和刪除操作外,絕大多數(shù)都是讀取操作距芬,而且讀操作在大多數(shù)時(shí)候都是成功的涝开。正是基于這個(gè)前提,ConcurrentHashMap 針對讀操作做了大量的優(yōu)化框仔。通過 HashEntry 對象的不變性和用 volatile 型變量協(xié)調(diào)線程間的內(nèi)存可見性舀武,使得 大多數(shù)時(shí)候,讀操作不需要加鎖就可以正確獲得值离斩。這個(gè)特性使得 ConcurrentHashMap 的并發(fā)性能在分離鎖的基礎(chǔ)上又有了近一步的提高银舱。
ConcurrentHashMap 是一個(gè)并發(fā)散列映射表的實(shí)現(xiàn)瘪匿,它允許完全并發(fā)的讀取,并且支持給定數(shù)量的并發(fā)更新寻馏。相比于 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap()))棋弥,ConcurrentHashMap 擁有更高的并發(fā)性。在 HashTable 和由同步包裝器包裝的 HashMap 中诚欠,使用一個(gè)全局的鎖來同步不同線程間的并發(fā)訪問顽染。同一時(shí)間點(diǎn),只能有一個(gè)線程持有鎖轰绵,也就是說在同一時(shí)間點(diǎn)粉寞,只能有一個(gè)線程能訪問容器。這雖然保證多線程間的安全并發(fā)訪問藏澳,但同時(shí)也導(dǎo)致對容器的訪問變成串行化的了仁锯。
ConcurrentHashMap 的高并發(fā)性主要來自于三個(gè)方面:
用分離鎖實(shí)現(xiàn)多個(gè)線程間的更深層次的共享訪問。
用 HashEntery 對象的不變性來降低執(zhí)行讀操作的線程在遍歷鏈表期間對加鎖的需求翔悠。
通過對同一個(gè) Volatile 變量的寫 / 讀訪問业崖,協(xié)調(diào)不同線程間讀 / 寫操作的內(nèi)存可見性。

使用分離鎖蓄愁,減小了請求 同一個(gè)鎖的頻率双炕。
通過 HashEntery 對象的不變性及對同一個(gè) Volatile 變量的讀 / 寫來協(xié)調(diào)內(nèi)存可見性,使得 讀操作大多數(shù)時(shí)候不需要加鎖就能成功獲取到需要的值撮抓。由于散列映射表在實(shí)際應(yīng)用中大多數(shù)操作都是成功的 讀操作妇斤,所以 2 和 3 既可以減少請求同一個(gè)鎖的頻率,也可以有效減少持有鎖的時(shí)間丹拯。通過減小請求同一個(gè)鎖的頻率和盡量減少持有鎖的時(shí)間 站超,使得 ConcurrentHashMap 的并發(fā)性相對于 HashTable 和用同步包裝器包裝的 HashMap有了質(zhì)的提高。

推薦文章:https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乖酬,一起剝皮案震驚了整個(gè)濱河市死相,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咬像,老刑警劉巖算撮,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異县昂,居然都是意外死亡肮柜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門倒彰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來审洞,“玉大人,你說我怎么就攤上這事待讳≡っ鳎” “怎么了缩赛?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長撰糠。 經(jīng)常有香客問我酥馍,道長,這世上最難降的妖魔是什么阅酪? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任旨袒,我火速辦了婚禮,結(jié)果婚禮上术辐,老公的妹妹穿的比我還像新娘砚尽。我一直安慰自己,他們只是感情好辉词,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布必孤。 她就那樣靜靜地躺著,像睡著了一般瑞躺。 火紅的嫁衣襯著肌膚如雪敷搪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天幢哨,我揣著相機(jī)與錄音赡勘,去河邊找鬼。 笑死捞镰,一個(gè)胖子當(dāng)著我的面吹牛闸与,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播岸售,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼践樱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了凸丸?” 一聲冷哼從身側(cè)響起拷邢,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎甲雅,沒想到半個(gè)月后解孙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坑填,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抛人,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了脐瑰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妖枚。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖苍在,靈堂內(nèi)的尸體忽然破棺而出绝页,到底是詐尸還是另有隱情荠商,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布续誉,位于F島的核電站莱没,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏酷鸦。R本人自食惡果不足惜饰躲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望臼隔。 院中可真熱鬧嘹裂,春花似錦、人聲如沸摔握。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氨淌。三九已至泊愧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宁舰,已是汗流浹背拼卵。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛮艰,地道東北人腋腮。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像壤蚜,于是被迫代替她去往敵國和親即寡。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

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