Map 綜述—徹頭徹尾理解 ConcurrentHashMap

寫在前面

本文全部關於 ConcurrentHashMap 的源碼都是基於 JDK 1.6 的,不一樣 JDK 版本之間會有些許差別,但不影響咱們對 ConcurrentHashMap 的數據結構、原理等總體的把握和了解java

ConcurrentHashMap是J.U.C(java.util.concurrent包)的重要成員,它是HashMap的一個線程安全的、支持高效併發的版本c++

ConcurrentHashMap 概述

HashMap 是 Java Collection Framework 的重要成員,也是Map族(以下圖所示)中咱們最爲經常使用的一種。不過遺憾的是,HashMap不是線程安全的數組

HashMap的這一缺點每每會形成諸多不便,雖然在併發場景下HashTable和由同步包裝器包裝的HashMap(Collections.synchronizedMap(Map<K,V> m) )能夠代替HashMap,可是它們都是經過使用一個全局的鎖來同步不一樣線程間的併發訪問,所以會帶來不可忽視的性能問題。慶幸的是,JDK爲咱們解決了這個問題,它爲HashMap提供了一個線程安全的高效版本 —— ConcurrentHashMap安全

在ConcurrentHashMap中,不管是讀操做仍是寫操做都能保證很高的性能:在進行讀操做時(幾乎)不須要加鎖,而在寫操做時經過鎖分段技術只對所操做的段加鎖而不影響客戶端對其它段的訪問。特別地,在理想狀態下,ConcurrentHashMap 能夠支持 16 個線程執行併發寫操做(若是併發級別設爲16),及任意數量線程的讀操做 bash

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 {
 ...
}
複製代碼

成員變量定義ui

與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),及任意數量線程的讀操做

本文到此結束,喜歡的朋友幫忙點點贊和關注,感謝!!!

相關文章
相關標籤/搜索