ConcurrentHashMap是支持多線程併發操做的哈希表,與HashTable類似,不支持null的key或value,方法聲明上也遵循了HashTable的規範。整體數據結構與HashMap相似,都是數組,按「鏈地址法」哈希具體的值。但ConcurrentHashMap內部按「段」來組織,每一個段對應了一個或多個哈希entry。寫操做(put,remove等)都須要加排它鎖,而讀操做(get)不須要加鎖,所以獲取的值多是讀操做的中間狀態,尤爲對(putAll和clear),讀操做可能只能獲取部分值。迭代器和enumeration返回的是哈希表某個狀態,不拋出ConcurrentModificationException。迭代器同一時刻只容許一個線程使用。 算法
ConcurrentHashMap的成員變量有: 數組
static final int DEFAULT_INITIAL_CAPACITY = 16; 數據結構
static final float DEFAULT_LOAD_FACTOR = 0.75f; 多線程
static final int DEFAULT_CONCURRENCY_LEVEL = 16; 併發
static final int MAXIMUM_CAPACITY = 1 << 30; app
static final int MIN_SEGMENT_TABLE_CAPACITY = 2; ssh
static final int MAX_SEGMENTS = 1 << 16; 函數
static final int RETRIES_BEFORE_LOCK = 2; this
其中大部分與HashMap一致。DEFAULT_CONCURRENCY_LEVEL必定程度上衡量了可支持的併發線程數;MIN_SEGMENT_TABLE_CAPACITY是每一個段最少的哈希entry,即每一個段中HashEntry數組最小容量;MAX_SEGMENTS是最大容許的段數目(源碼中註釋說是不小於1<<24,但默認的值給的是1<<16,而且對該值註釋說」slightly conservative」);RETRIES_BEFORE_LOCK是size和containsValue方法加鎖時重試的最大次數,若是在屢次重試後仍沒有獲取鎖,則線程進入中斷狀態在加鎖隊列中排隊。 spa
簡單來講,ConcurrentHashMap是一個Segment<K,V>[]數組,每一個Segment<K,V>又包含一個HashEntry<K,V>[]數組(即維護了一個小的hash表),HashEntry數組每一個元素就是一個hash值對應的HashEntry鏈,具體的key-value對就放在這個鏈中。
先來講說Segment<K,V>。這是一個內部類,派生自ReentrantLock。主要的成員變量包括:
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
其中,loadFactor和threshold與HashMap中一致;table就是該段一個小的hash表;count是表中的元素個數;modCount是段中全部可變操做(put,remove,clear等)的次數,在isEmpty和size中用獲得,若是值溢出可能會致使一些問題。
段的主要操做是put,remove,replace,clear。除了clear,每次操做都須要tryLock,若是未得到鎖,則調用scanAndLock或scanAndLockForPut,一方面繼續請求得到獨佔鎖,另外一方面檢索表以找到待操做的節點(The main benefit is to absorb cache misses (which are very common for hash tables) while obtaining locks so that traversal is faster once)。若是屢次(最大爲MAX_SCAN_RETRIES)請求都沒能得到鎖,則interrupt線程,進入隊列等待直到得到鎖。具體細節與ReentrantLock的實現有關。相比而言,clear操做就顯得粗暴一點,直接調用lock加鎖。此外,put可能致使rehash,也是以兩倍的大小增加。
再來看看ConcurrentHashMap本身的成員。ConcurrentHashMap最關鍵的構造函數:
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel)
首先找到一個不小於concurrentcyLevel的數,這個數即ssize必須是2的冪,且2^sshift=ssize,由此獲得segmentShift和segmentMask。
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
再看cap。cap是每一個段中的表初始大小,cap也是2的冪,最小是2。cap×sszie>=initialCapacity。
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
最後,建立段數組。s0比如一個段模板,初始化表大小爲cap,rehash的threshold爲cap×loadFactor,根據s0建立段數組。
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
所以,concurrentHashMap有ssize個段,即表示最多支持ssize個線程同時寫。
concurrentHashMap的方法中,不少都有一個「recheck」的過程。
如isEmpty()方法,先檢查每一個段的count,若是有不爲0直接返回false,不然累加modCount獲得sum;接着再遍歷一遍段,若是count有不爲0則返回false,不然用以前的sum遞減modCount,若最後sum不爲0則返回false,不然爲true。這樣作是針對一個段進行檢查的同時另外一個段正在進行修改的狀況,意義和做用我還不是特別能理解。isEmpty並不須要加鎖。
Size()操做都須要對每一個段進行加鎖(也不必定,若是發現爲空則直接返回0);
containsValue和size同樣,在指定次數(RETRIES_BEFORE_LOCK)內沒發現想要的值,則強制加鎖進行排它性查找,若是找到則返回true;還有個contains,是爲了保持與HashTable一致,實際調用的就是containsValue。
get和containsKey都不須要加鎖,表面含義和代碼都很容易理解,若是參數爲null則拋出NullPointerException。
put操做首先找到段數組中指定映射的段,沒有則生成一個新的段,接着調用段的put方法插入數據,每次在鏈頭部插入。
putIfAbsent與put差很少,不一樣的是隻有在不存在指定key時纔會插入新的key-value值,若是已經有了這個key則不執行更新。
putAll調用put進行插入;
remove調用segment的remove;
replace調用segment的replace;
clear依次調用每一個段的clear。clear並不會同時對全部段加鎖,所以可能在執行clear操做時,有讀操做發生會出現讀取到clear操做的中間結果,putAll也有這種狀況。
keySet返回一個key的set,該set有個弱一致性的迭代器,不拋出ConcurrentModificationException。迭代器的操做調用的是該concurrentHashMap相關的方法,包括remove,size,contains,clear,isEmpty等,不包括任何put操做。全部iterator上的改動都會直接反應到ConcurrentHashMap,反之亦然。
values,entrySet同keySet;elements和keys返回的是一個枚舉集。
再說hash算法,HashMap的hash算法已經夠變態了,沒想到一山還有一山高,ConcurrentHashMap也是特麼的嚇唬人啊。
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
最後,ConcurrentHashMap的實現比起HashMap複雜不少,還有不少概念我理解的還很不到位,例如volatile的用法,「happen-before」究竟是什麼意思?不加鎖的讀會出現什麼可能意想不到的後果?等等。