Map 綜述—徹頭徹尾理解 ConcurrentHashMap

java閘瓦發表於2019-04-09

寫在前面

本文所有關於 ConcurrentHashMap 的原始碼都是基於 JDK 1.6 的,不同 JDK 版本之間會有些許差異,但不影響我們對 ConcurrentHashMap 的資料結構、原理等整體的把握和了解

ConcurrentHashMap是J.U.C(java.util.concurrent包)的重要成員,它是HashMap的一個執行緒安全的、支援高效併發的版本

ConcurrentHashMap 概述

HashMap 是 Java Collection Framework 的重要成員,也是Map族(如下圖所示)中我們最為常用的一種。不過遺憾的是,HashMap不是執行緒安全的

HashMap的這一缺點往往會造成諸多不便,雖然在併發場景下HashTable和由同步包裝器包裝的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它們都是通過使用一個全域性的鎖來同步不同執行緒間的併發訪問,因此會帶來不可忽視的效能問題。慶幸的是,JDK為我們解決了這個問題,它為HashMap提供了一個執行緒安全的高效版本 —— ConcurrentHashMap

在ConcurrentHashMap中,無論是讀操作還是寫操作都能保證很高的效能:在進行讀操作時(幾乎)不需要加鎖,而在寫操作時通過鎖分段技術只對所操作的段加鎖而不影響客戶端對其它段的訪問。特別地,在理想狀態下,ConcurrentHashMap 可以支援 16 個執行緒執行併發寫操作(如果併發級別設為16),及任意數量執行緒的讀操作

Map 綜述—徹頭徹尾理解 ConcurrentHashMap
ConcurrentHashMap的高效併發機制是通過以下三方面來保證的

  • 通過鎖分段技術保證併發環境下的寫操作;
  • 通過 HashEntry的不變性、Volatile變數的記憶體可見性和加鎖重讀機制保證高效、安全的讀操作;
  • 通過不加鎖和加鎖兩種方案控制跨段操作的的安全性。

ConcurrentHashMap 在 JDK 中的定義

ConcurrentHashMap類中包含兩個靜態內部類

  • HashEntry: 用來封裝具體的K/V對,是個典型的四元組
  • Segment:用來充當鎖的角色,每個 Segment 物件守護整個ConcurrentHashMap的若干個桶 (可以把Segment看作是一個小型的雜湊表),其中每個桶是由若干個 HashEntry 物件連結起來的連結串列

ConcurrentHashMap 在預設併發級別下會建立16個Segment物件的陣列,如果鍵能均勻雜湊,每個 Segment 大約守護整個雜湊表中桶總數的 1/16。

類結構定義

ConcurrentHashMap 繼承了AbstractMap並實現了ConcurrentMap介面,其在JDK中的定義為:

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
 implements ConcurrentMap<K, V>, Serializable {
 ...
}
複製程式碼

成員變數定義

與HashMap相比,ConcurrentHashMap 增加了兩個屬性用於定位段,分別是 segmentMask 和 segmentShift。此外,不同於HashMap的是,ConcurrentHashMap底層結構是一個Segment陣列,而不是Object陣列

final int segmentMask; // 用於定位段,大小等於segments陣列的大小減 1,是不可變的

final int segmentShift; // 用於定位段,大小等於32(hash值的位數)減去對segments的大小取以2為底的對數值,是不可變的

final Segment<K,V>[] segments; // ConcurrentHashMap的底層結構是一個Segment陣列
複製程式碼

段的定義:Segment

Segment 類繼承於 ReentrantLock 類,從而使得 Segment 物件能充當鎖的角色

Map 綜述—徹頭徹尾理解 ConcurrentHashMap

在Segment類中,count 變數是一個計數器,它表示每個 Segment 物件管理的 table 陣列包含的 HashEntry 物件的個數,也就是 Segment 中包含的 HashEntry 物件的總數。特別需要注意的是,之所以在每個 Segment 物件中包含一個計數器,而不是在 ConcurrentHashMap 中使用全域性的計數器,是對 ConcurrentHashMap 併發性的考慮:因為這樣當需要更新計數器時,不用鎖定整個ConcurrentHashMap

基本元素:HashEntry

與HashMap中的Entry類似,HashEntry也包括同樣的四個域,分別是key、hash、value和next。不同的是,在HashEntry類中,key,hash和next域都被宣告為final的,value域被volatile所修飾,因此HashEntry物件幾乎是不可變的,這是ConcurrentHashmap讀操作並不需要加鎖的一個重要原因

由於value域被volatile修飾,所以其可以確保被讀執行緒讀到最新的值,這是ConcurrentHashmap讀操作並不需要加鎖的另一個重要原因。

ConcurrentHashMap 的建構函式

1,ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

該建構函式意在構造一個具有指定容量、指定負載因子和指定段數目/併發級別(若不是2的冪次方,則會調整為2的冪次方)的空ConcurrentHashMap

2,ConcurrentHashMap(int initialCapacity, float loadFactor)

該建構函式意在構造一個具有指定容量、指定負載因子和預設併發級別(16)的空ConcurrentHashMap

3,ConcurrentHashMap(int initialCapacity)

該建構函式意在構造一個具有指定容量、預設負載因子(0.75)和預設併發級別(16)的空ConcurrentHashMap

4,ConcurrentHashMap()

該建構函式意在構造一個具有預設初始容量(16)、預設負載因子(0.75)和預設併發級別(16)的空ConcurrentHashMap

5,ConcurrentHashMap(Map<? extends K, ? extends V> m)

該建構函式意在構造一個與指定 Map 具有相同對映的 ConcurrentHashMap,其初始容量不小於 16 (具體依賴於指定Map的大小),負載因子是 0.75,併發級別是 16, 是 Java Collection Framework 規範推薦提供的

小結

在這裡,我們提到了三個非常重要的引數:初始容量負載因子併發級別,這三個引數是影響ConcurrentHashMap效能的重要引數

ConcurrentHashMap 的資料結構

通過使用段(Segment)將ConcurrentHashMap劃分為不同的部分,ConcurrentHashMap就可以使用不同的鎖來控制對雜湊表的不同部分的修改,從而允許多個修改操作併發進行, 這正是ConcurrentHashMap鎖分段技術的核心內涵

ConcurrentHashMap 的併發存取

在ConcurrentHashMap中,執行緒對對映表做讀操作時,一般情況下不需要加鎖就可以完成,對容器做結構性修改的操作(比如,put操作、remove操作等)才需要加鎖。

用分段鎖機制實現多個執行緒間的併發寫操作: put(key, vlaue)

在ConcurrentHashMap中,典型結構性修改操作包括put、remove和clear,下面我們首先以put操作為例說明對ConcurrentHashMap做結構性修改的過程

public V put(K key, V value) {
 if (value == null)
 throw new NullPointerException();
 int hash = hash(key.hashCode());
 return segmentFor(hash).put(key, hash, value, false);
 }
複製程式碼

ConcurrentHashMap不同於HashMap,它既不允許key值為null,也不允許value值為null

定位段的segmentFor()方法原始碼如下

final Segment<K,V> segmentFor(int hash) {
 return segments[(hash >>> segmentShift) & segmentMask];
 }
複製程式碼

根據key的hash值的高n位就可以確定元素到底在哪一個Segment中

段的put()方法的原始碼如下所示

V put( K key, int hash, V value, boolean onlyIfAbsent )
{
	lock();                                                                                 /* 上鎖 */
	try {
		int c = count;
		if ( c++ > threshold )                                                          /* ensure capacity */
			rehash();
		HashEntry<K, V>[] tab = table;                                                  /* table是Volatile的 */
		int		index	= hash & (tab.length - 1);                              /* 定位到段中特定的桶 */
		HashEntry<K, V> first	= tab[index];                                           /* first指向桶中連結串列的表頭 */
		HashEntry<K, V> e	= first;
		/* 檢查該桶中是否存在相同key的結點 */
		while ( e != null && (e.hash != hash || !key.equals( e.key ) ) )
			e = e.next;
		V oldValue;
		if ( e != null )                                                                /* 該桶中存在相同key的結點 */
		{
			oldValue = e.value;
			if ( !onlyIfAbsent )
				e.value = value;                                                /* 更新value值 */
		}else {                                                                         /* 該桶中不存在相同key的結點 */
			oldValue = null;
			++modCount;                                                             /* 結構性修改,modCount加1 */
			tab[index]	= new HashEntry<K, V>( key, hash, first, value );       /* 建立HashEntry並將其鏈到表頭 */
			count		= c;                                                    /* write-volatile,count值的更新一定要放在最後一步(volatile變數) */
		}
		return(oldValue);                                                               /* 返回舊值(該桶中不存在相同key的結點,則返回null) */
	} finally {
		unlock();                                                                       /* 在finally子句中解鎖 */
	}
}
複製程式碼

相比較於 HashTable 和由同步包裝器包裝的HashMap每次只能有一個執行緒執行讀或寫操作,ConcurrentHashMap 在併發訪問效能上有了質的提高。在理想狀態下,ConcurrentHashMap 可以支援 16 個執行緒執行併發寫操作(如果併發級別設定為 16),及任意數量執行緒的讀操作

相關文章