ConcurrentHashMap 的實現原理

我們了解到關于 HashMap 和 Hashtable 這兩種集合。其中 HashMap 是非線程安全的,當我們只有一個線程在使用 HashMap 的時候,自然不會有問題,但如果涉及到多個線程,并且有讀有寫的過程中,HashMap 就不能滿足我們的需要了(fail-fast)。在不考慮性能問題的時候,我們的解決方案有 Hashtable 或者Collections.synchronizedMap(hashMap),這兩種方式基本都是對整個 hash 表結構做鎖定操作的,這樣在鎖表的期間,別的線程就需要等待了,無疑性能不高。
所以我們學習一個 util.concurrent 包的重要成員,ConcurrentHashMap。
ConcurrentHashMap 的實現是依賴于 Java 內存模型,所以我們在了解 ConcurrentHashMap 的前提是必須了解Java 內存模型。但 Java 內存模型并不是本文的重點,所以我假設讀者已經對 Java 內存模型有所了解。
ConcurrentHashMap 分析
ConcurrentHashMap 的結構是比較復雜的,都深究去本質,其實也就是數組和鏈表而已。我們由淺入深慢慢的分析其結構。
先簡單分析一下,ConcurrentHashMap 的成員變量中,包含了一個 Segment 的數組(final Segment<K,V>[] segments;
),而 Segment 是 ConcurrentHashMap 的內部類,然后在 Segment 這個類中,包含了一個 HashEntry 的數組(transient volatile HashEntry<K,V>[] table;
)。而 HashEntry 也是 ConcurrentHashMap 的內部類。HashEntry 中,包含了 key 和 value 以及 next 指針(類似于 HashMap 中 Entry),所以 HashEntry 可以構成一個鏈表。
所以通俗的講,ConcurrentHashMap 數據結構為一個 Segment 數組,Segment 的數據結構為 HashEntry 的數組,而 HashEntry 存的是我們的鍵值對,可以構成鏈表。
首先,我們看一下 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 的學習可以類比著 HashMap 中的 Entry。我們的存儲鍵值對的過程中,散列的時候如果發生“碰撞”,將采用“分離鏈表法”來處理碰撞:把碰撞的 HashEntry 對象鏈接成一個鏈表。
如下圖,我們在一個空桶中插入 A、B、C 兩個 HashEntry 對象后的結構圖(其實應該為鍵值對,在這進行了簡化以方便更容易理解):


圖1

Segment

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

。其繼承于 ReentrantLock 類,從而使得 Segment 對象可以充當鎖的角色。Segment 中包含HashEntry 的數組,其可以守護其包含的若干個桶(HashEntry的數組)。Segment 在某些意義上有點類似于 HashMap了,都是包含了一個數組,而數組中的元素可以是一個鏈表。
table:table 是由 HashEntry 對象組成的數組如果散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式鏈接成一個鏈表table數組的數組成員代表散列映射表的一個桶每個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分如果并發級別為 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16。
count 變量是計算器,表示每個 Segment 對象管理的 table 數組(若干個 HashEntry 的鏈表)包含的HashEntry 對象的個數。之所以在每個Segment對象中包含一個 count 計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是為了避免出現“熱點域”而影響并發性。

/** 
  * 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 三個節點后,Segment 的示意圖:


圖2

其實從我個人角度來說,Segment結構是與HashMap很像的。
ConcurrentHashMap
ConcurrentHashMap 的結構中包含的 Segment 的數組,在默認的并發級別會創建包含 16 個 Segment 對象的數組。通過我們上面的知識,我們知道每個 Segment 又包含若干個散列表的桶,每個桶是由 HashEntry 鏈接起來的一個鏈表。如果 key 能夠均勻散列,每個 Segment 大約守護整個散列表桶總數的 1/16。
下面我們還有通過一個圖來演示一下 ConcurrentHashMap 的結構:


圖3

并發寫操作
在 ConcurrentHashMap 中,當執行 put 方法的時候,會需要加鎖來完成。我們通過代碼來解釋一下具體過程:當我們 new 一個 ConcurrentHashMap 對象,并且執行put操作的時候,首先會執行 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 不允許空值。該方法首先有一個 Segment 的引用 s,然后會通過 hash() 方法對 key 進行計算,得到哈希值;繼而通過調用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進行存儲操作。該方法源碼為:

final V put(K key, int hash, V value, boolean onlyIfAbsent) { 
    //加鎖,這里是鎖定的Segment而不是整個  
    ConcurrentHashMap HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
    V oldValue; try { HashEntry<K,V>[] tab = table; 
    //得到hash對應的table中的索引index 
    int index = (tab.length - 1) & hash; 
   //找到hash對應的是具體的哪個桶,也就是哪個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;
  }

關于該方法的某些關鍵步驟,在源碼上加上了注釋。
需要注意的是:加鎖操作是針對的 hash 值對應的某個 Segment,而不是整個 ConcurrentHashMap。因為 put 操作只是在這個 Segment 中完成,所以并不需要對整個 ConcurrentHashMap 加鎖。所以,此時,其他的線程也可以對另外的 Segment 進行 put 操作,因為雖然該 Segment 被鎖住了,但其他的 Segment 并沒有加鎖。同時,讀線程并不會因為本線程的加鎖而阻塞。
正是因為其內部的結構以及機制,所以 ConcurrentHashMap 在并發訪問的性能上要比Hashtable和同步包裝之后的HashMap的性能提高很多。在理想狀態下,ConcurrentHashMap 可以支持 16 個線程執行并發寫操作(如果并發級別設置為 16),及任意數量線程的讀操作。
總結
在實際的應用中,散列表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基于這個前提,ConcurrentHashMap 針對讀操作做了大量的優化。通過 HashEntry 對象的不變性和用 volatile 型變量協調線程間的內存可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的并發性能在分離鎖的基礎上又有了近一步的提高。
ConcurrentHashMap 是一個并發散列映射表的實現,它允許完全并發的讀取,并且支持給定數量的并發更新。相比于 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的并發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全局的鎖來同步不同線程間的并發訪問。同一時間點,只能有一個線程持有鎖,也就是說在同一時間點,只能有一個線程能訪問容器。這雖然保證多線程間的安全并發訪問,但同時也導致對容器的訪問變成串行化的了。
ConcurrentHashMap 的高并發性主要來自于三個方面:
用分離鎖實現多個線程間的更深層次的共享訪問。
用 HashEntery 對象的不變性來降低執行讀操作的線程在遍歷鏈表期間對加鎖的需求。
通過對同一個 Volatile 變量的寫 / 讀訪問,協調不同線程間讀 / 寫操作的內存可見性。

使用分離鎖,減小了請求 同一個鎖的頻率。
通過 HashEntery 對象的不變性及對同一個 Volatile 變量的讀 / 寫來協調內存可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由于散列映射表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。通過減小請求同一個鎖的頻率和盡量減少持有鎖的時間 ,使得 ConcurrentHashMap 的并發性相對于 HashTable 和用同步包裝器包裝的 HashMap有了質的提高。

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

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

推薦閱讀更多精彩內容