一、前言
ConcurrentHashMap
是執行緒安全並且高效的HashMap
,其它的類似容器有以下缺點:
HashMap
在併發執行put
操作時,會導致Entry
連結串列形成環形資料結構,就會產生死迴圈獲取Entry
。HashTable
使用synchronized
來保證執行緒安全,但線上程競爭激烈的情況下HashTable
的效率非常低下。
ConcurrentHashMap
高效的原因在於它採用 鎖分段技術,首先將資料分成一段一段地儲存,然後給每段資料配一把鎖,當一個執行緒佔用鎖並且訪問一段資料的時候,其他段的資料也能被其他執行緒訪問。
二、 ConcurrentHashMap 的結構
ConcurrentHashMap
是由Segment
陣列結構和HashEntry
陣列結構組成:
Segment
是一種可重入鎖,在ConcurrentHashMap
裡面扮演鎖的角色。HashEntry
則用於儲存鍵值對資料。
一個ConcurrentHashMap
裡包含一個Segment
陣列,它的結構和HashMap
類似,是一種陣列和連結串列結構。
Segment
裡包含一個HashEntry
陣列,每個HashEntry
是一個連結串列結構的元素,每個Segment
守護著一個HashEntry
裡的元素,當對HashEntry
陣列的資料進行修改時,必須首先獲得與它對應的Segment
鎖。
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
HashEntry 結構
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}
複製程式碼
2.1 初始化
ConcurrentHashMap
的初始化方法是通過initialCapacity
、loadFactor
和concurrencyLevel
等幾個引數來初始化segment
陣列、段偏移量segmentShift
、段掩碼segmentMask
和每個segment
裡的HashEntry
來實現的。
2.1.1 初始化 segment 陣列
初始化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);
複製程式碼
2.1.2 初始化每個 segment
輸入引數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
。
2.2 定位 segment
在插入和獲取元素的時候,必須先通過雜湊演算法定位到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
上,從而提高容器的存取效率。
2.3 操作
2.3.1 get 操作
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;
複製程式碼
2.3.2 put 操作
由於put
方法需要對共享變數進行寫入,所以為了執行緒安全,在操作共享變數時必須加鎖。put
方法首先定位到Segment
,然後在Segment
裡進行插入操作。插入操作需要經歷兩個步驟:
- 判斷是否需要對
Segment
裡的HashEntry
陣列進行擴容 - 定位新增元素的位置,然後將其放在
HashEntry
陣列裡
2.3.3 size 操作
如果要統計整個ConcurrentHashMap
裡元素的大小,就必須統計所有Segment
元素的大小後求和,雖然每個Segment
的全域性變數count
是一個volatile
變數,在相加時可以獲取最新值,但是不能保證之前累加過的Segment
大小不發生變化。
因此,ConcurrentHashMap
會先嚐試2
次通過不鎖住Segment
的方式來統計各個Segment
大小,如果統計的過程中,容器的count
發生了變化,則再採用加鎖的方式來統計所有Segment
的大小。
檢測容器大小是否發生變化的原理為:在put
、remove
和clean
方法裡操作元素前會將變數modCount
進行加1
,那麼在統計size
前後比較modCount
是否發生變化,從而得知容器的大小是否發生變化。
三、參考文獻
<<Java
併發程式設計的藝術>> - Java
併發容器和框架