ConcurrentHashMap
是線程安全而且高效的HashMap
,其它的相似容器有如下缺點:java
HashMap
在併發執行put
操做時,會致使Entry
鏈表造成環形數據結構,就會產生死循環獲取Entry
。HashTable
使用synchronized
來保證線程安全,但在線程競爭激烈的狀況下HashTable
的效率很是低下。ConcurrentHashMap
高效的緣由在於它採用 鎖分段技術,首先將數據分紅一段一段地存儲,而後給每段數據配一把鎖,當一個線程佔用鎖而且訪問一段數據的時候,其餘段的數據也能被其餘線程訪問。算法
ConcurrentHashMap
是由Segment
數組結構和HashEntry
數組結構組成:編程
Segment
是一種可重入鎖,在ConcurrentHashMap
裏面扮演鎖的角色。HashEntry
則用於存儲鍵值對數據。一個ConcurrentHashMap
裏包含一個Segment
數組,它的結構和HashMap
相似,是一種數組和鏈表結構。 數組
Segment
裏包含一個
HashEntry
數組,每一個
HashEntry
是一個鏈表結構的元素,每一個
Segment
守護着一個
HashEntry
裏的元素,當對
HashEntry
數組的數據進行修改時,必須首先得到與它對應的
Segment
鎖。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
}
複製代碼
count
:Segment
中元素的數量modCount
:對table
的大小形成影響的操做的數量threshold
:閾值,Segment
裏面元素的數量超過這個值依舊就會對Segment
進行擴容table
:鏈表數組,數組中的每個元素表明了一個鏈表的頭部loadFactor
:負載因子,用於肯定threshold
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
複製代碼
ConcurrentHashMap
的初始化方法是經過initialCapacity
、loadFactor
和concurrencyLevel
等幾個參數來初始化segment
數組、段偏移量segmentShift
、段掩碼segmentMask
和每一個segment
裏的HashEntry
來實現的。安全
初始化segment
的源代碼以下,它會計算出:數據結構
ssize
:segment
數組的長度segmentShift
:sshift
等於ssize
從1
向左移位的次數,segmentShift
等於32-sshift
,segmentShift
用於 定位參與散列運算的位數segmentMask
:散列運算的掩碼,等於ssize-1
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
//計算 segments 數組的長度,它是大於等於 concurrencyLevel 的最小的 2 的 N 次方。
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize);
複製代碼
輸入參數initialCapacity
是ConcurrentHashMap
的初始化容量,loadFactor
是每一個segment
的負載因子,在構造方法裏經過這兩個參數來初始化數組中的每一個segment
。多線程
if (initialCapacity < MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
}
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity) {
++c;
}
int cap = 1;
while (cap < c) {
cap <<= 1;
}
for (int i = 0; i < this.segments.length; i++) {
this.segments[i] = new Segment<K, V>(cap, loadFactor);
}
複製代碼
cap 是 segment 裏 HashEntry 數組的長度,它等於initialCapacity / ssize
,若是c
大於1
,就會取大於等於c
的2
的N
次方。segment
的容量threshold
等於(int) cap * loadFactor
,默認狀況下initialCapacity
等於16
,ssize
等於16
,loadFactor
等於0.75
,所以cap
等於1
,threshold
等於0
。併發
在插入和獲取元素的時候,必須先經過散列算法定位到Segment
,ConcurrentHashMap
會首先對元素的hashCode()
進行一次再散列。框架
private static int hash(int h) {
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
複製代碼
再散列的目的是減小散列衝突,使元素可以均勻地分佈在不一樣的Segment
上,從而提升容器的存取效率。ssh
segment
的get
操做過程爲:先進行一次再散列,而後使用這個散列值經過散列運算定位到Segment
,再經過散列算法定位到元素。
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
複製代碼
get
操做的高效之處在於整個get
過程不須要加鎖,除非讀到的值爲空才加鎖重讀。在它的get
方法裏,將要使用的共享變量都定義成volatile
類型,如用於統計當前segment
大小的count
字段和用於存儲值的HashEntry
的value
,定義成volatile
的變量,可以在線程之間保持可見性,可以被多線程同時讀,而且保證不會讀到過時的值,在get
操做裏,只須要讀而不須要寫共享變量count
和value
,因此能夠不用加鎖。
transient volatile int count;
volatile V value;
複製代碼
因爲put
方法須要對共享變量進行寫入,因此爲了線程安全,在操做共享變量時必須加鎖。put
方法首先定位到Segment
,而後在Segment
裏進行插入操做。插入操做須要經歷兩個步驟:
Segment
裏的HashEntry
數組進行擴容HashEntry
數組裏若是要統計整個ConcurrentHashMap
裏元素的大小,就必須統計全部Segment
元素的大小後求和,雖然每一個Segment
的全局變量count
是一個volatile
變量,在相加時能夠獲取最新值,可是不能保證以前累加過的Segment
大小不發生變化。
所以,ConcurrentHashMap
會先嚐試2
次經過不鎖住Segment
的方式來統計各個Segment
大小,若是統計的過程當中,容器的count
發生了變化,則再採用加鎖的方式來統計全部Segment
的大小。
檢測容器大小是否發生變化的原理爲:在put
、remove
和clean
方法裏操做元素前會將變量modCount
進行加1
,那麼在統計size
先後比較modCount
是否發生變化,從而得知容器的大小是否發生變化。
<<Java
併發編程的藝術>> - Java
併發容器和框架