注:本文根據網絡和部分書籍整理基於JDK1.7書寫,如有雷同敬請諒解 歡迎指正文中的錯誤之處。
數據結構
ConcurrentHashMap是HashMap的一個線程安全的、支持高效併發的版本。在多線程併發場景下HashTable和由同步包裝器包裝的HashMap,都是通過使用一個全局的鎖來同步不同線程間的併發訪問,會帶來不可忽視的性能問題。
ConcurrentHashMap採用分段鎖的設計Segment + HashEntry的方式進行實現,採用table數組+單向鏈表的數據結構,結構如下:
重要屬性
initialCapacity:初始容量(默認16),這個值指的是整個 ConcurrentHashMap 的初始容量,實際操作的時候需要平均分給每個 Segment。
concurrencyLevel:併發數(Segment 數,默認16)。ConcurrentHashMap採用了分段鎖的設計,分段鎖個數即Segment[]的數組長度,只有在同一個分段內才存在競態關係,不同的分段鎖之間沒有鎖競爭,所以當它們分別操作不同的 Segment時最多可以同時支持16個線程併發。
注:用戶可以在構造函數中初始設置爲其他值,當用戶設置併發度時,ConcurrentHashMap會使用大於等於該值的最小2冪指數作爲實際併發度(如用戶設置併發度爲17,實際併發度則爲32),但是一旦初始化以後,它是不可以擴容的。
不能超過規定的最大值:final int MAX_SEGMENTS = 1 << 16
segments:Segment 通過繼承 ReentrantLock 來進行加鎖,每次需要加鎖的操作鎖住的是一個 segment,每個 Segment 對象用來守護其(成員對象 table 中)包含的若干個桶,這樣保證每個 Segment 是線程安全的,也就實現了全局的線程安全。
注:JDK7中除了第一個Segment之外,剩餘的Segment採用的是延遲初始化的機制:每次put之前都需要檢查key對應的Segment是否爲null,如果是則調用ensureSegment()以確保對應的Segment被創建。Segment[i] 的默認大小爲 2,負載因子是 0.75
table: 由 HashEntry 對象組成的數組。table 數組的每一個數組成員就是散列映射表的一個桶。
count:計數器,它表示每個 Segment 對象管理的 table 數組(若干個 HashEntry 組成的鏈表)包含的 HashEntry 對象的個數。每一個 Segment 對象都有一個 count 對象來表示本 Segment 中包含的 HashEntry 對象的總數。
注:之所以在每個 Segment 對象中包含一個計數器,而不在 ConcurrentHashMap 中使用全局的計數器,是爲了避免出現“熱點域”而影響 ConcurrentHashMap 的併發性。需要更新計數器時,不用鎖定整個 ConcurrentHashMap。
loadFactor:負載因子(默認0.75),之前我們說了,Segment 數組不可以擴容,所以這個負載因子是給每個 Segment 內部使用的。
put操作
1、判斷value是否爲null,如果爲null,直接拋出異常。注:不允許key或者value爲null
2、通過哈希算法定位到Segment(key通過一次hash運算得到一個hash值,將得到hash值向右按位移動segmentShift位,然後再與segmentMask做&運算得到segment的索引j)。
3、使用Unsafe的方式從Segment數組中獲取該索引對應的Segment對象
4、向這個Segment對象中put值
注:對共享變量進行寫入操作爲了線程安全,在操作共享變量時必須得加鎖,持有段鎖(鎖定整個segment)的情況下執行的。修改數據是不能併發進行的
判斷該值的插入是否會導致該 segment 的元素個數超過閾值,以確保容量不足時能夠rehash擴容,再插值。
注:rehash 擴容 segment 數組不能擴容,擴容的是 segment 數組某個位置內部的數組 HashEntry[] 擴容爲原來的 2 倍。先進行擴容,再插值
查找是否存在同樣一個key的結點,存在直接替換這個結點的值。否則創建一個新的結點並添加到hash鏈的頭部,修改modCount和count的值,修改count的值一定要放在最後一步。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
get操作
1、計算 hash 值,找到 segment 數組中的具體位置,使用Unsafe獲取對應的Segment
2、根據 hash 找到數組中具體的位置
3、從鏈表頭開始遍歷整個鏈表(因爲Hash可能會有碰撞,所以用一個鏈表保存),如果找到對應的key,則返回對應的value值,否則返回null。
注:get操作不需要鎖,由於其中涉及到的共享變量都使用volatile修飾,volatile可以保證內存可見性,所以不會讀取到過期數據。
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code key.equals(k)},
* then this method returns {@code v}; otherwise it returns
* {@code null}. (There can be at most one such mapping.)
*
* @throws NullPointerException if the specified key is null
*/
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
size()操作
size操作需要遍歷所有的Segment才能算出整個Map的大小。先採用不加鎖的方式,循環所有的Segment(通過Unsafe的getObjectVolatile()以保證原子讀語義)連續計算元素的個數,最多計算3次:
1、如果前後兩次計算結果相同,則說明計算出來的元素個數是準確的;
2、如果前後兩次計算結果都不同,則給每個Segment進行加鎖,再計算一次元素的個數;
注:在put , remove和clean方法裏操作元素前都會將變量modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。
/**
* Returns the number of key-value mappings in this map. If the
* map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
* <tt>Integer.MAX_VALUE</tt>.
*
* @return the number of key-value mappings in this map
*/
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}