數據結構
ConcurrentHashMap 實現并發操作的原理
使用了鎖分段技術:ConcurrentHashMap持有一組鎖(segment[]),并將數據盡可能分散在不同的鎖段中(即,每個鎖只會控制部分的數據HashEntry[])。這樣如果寫操作的數據分布在不同的鎖中,那么寫操作將可并行操作。因此來實現一定數量(即,鎖數量)并發線程修改。
同時通過Unsafe.putOrderedObject、UNSAFE.getObjectVolatile(??這兩個方法很重要,下文會介紹)來操作segment[]、HashEntry[]的元素使得在提升了性能的情況下在并發環境下依舊能獲取到最新的數據,同時HashEntry的value為volatile屬性,從而實現不加鎖的進行并發的讀操作,并且對該并發量并無限制。
注意,中不使用volatile的屬性來實現segment[]和HashEntry[]在多線程間的可見性。因為如果是修改操作,則在釋放鎖的時候就會將當前線程緩存中的數據寫到主存中,所以就無需在修改操作的過程中因修改volatile屬性字段而頻繁的寫線程內存數據到主存中。
源碼解析
重要屬性
//散列映射表的默認初始容量為 16。
static final int DEFAULT_INITIAL_CAPACITY = 16;
//散列映射表的默認裝載因子為 0.75,用于表示segment中包含的HashEntry元素的個數與HashEntry[]數組長度的比值。當某個segment中包含的HashEntry元素的個數超過了HashEntry[]數組的長度與裝載因子的乘積時,將觸發擴容操作。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//散列表的默認并發級別為 16。該值表示segment[]數組的長度,也就是鎖的總數。
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//散列表的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//segment中HashEntry[]數組最小長度
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//散列表的最大段數,也就是segment[]數組的最大長度
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
//在執行size()和containsValue(value)操作時,ConcurrentHashMap的做法是先嘗試 RETRIES_BEFORE_LOCK 次( 即,2次 )通過不鎖住segment的方式來統計、查詢各個segment,如果2次執行過程中,容器的modCount發生了變化,則再采用加鎖的方式來操作所有的segment
static final int RETRIES_BEFORE_LOCK = 2;
//segmentMask用于定位segment在segment[]數組中的位置。segmentMask是與運算的掩碼,等于segment[]數組size減1,掩碼的二進制各個位的值都是1( 因為,數組長度總是2^N )。
final int segmentMask;
//segmentShift用于決定H(key)參與segmentMask與運算的位數(高位),這里指在從segment[]數組定位segment:通過key的哈希結果的高位與segmentMask進行與運算哈希的結果。(詳見下面舉例)
final int segmentShift;
//Segment 類繼承于 ReentrantLock 類,從而使得 Segment 對象能充當鎖的角色。
final ConcurrentHashMap.Segment<K, V>[] segments;
重要對象
- ConcurrentHashMap.Segment
Segment 類繼承于 ReentrantLock 類,從而使得 Segment 對象能充當鎖的角色,并且是一個可重入鎖。每個 Segment 對象維護其包含的若干個桶(即,HashEntry[])。
//最大自旋次數,若是單核則為1,多核則為64。該字段用于scanAndLockForPut、scanAndLock方法
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
/**
* table 是由 HashEntry 對象組成的數組
* 如果散列時發生碰撞,碰撞的 HashEntry 對象就以鏈表的形式鏈接成一個鏈表
* table 數組的元素代表散列映射表的一個桶
* 每個 table 守護整個 ConcurrentHashMap 數據總數的一部分
* 如果并發級別為 16,table 則維護 ConcurrentHashMap 數據總數的 1/16
*/
transient volatile HashEntry<K,V>[] table;
//segment中HashEntry的總數。 PS:注意JDK 7中該字段不是volatile的
transient int count;
//segment中數據被更新的次數
transient int modCount;
//當table中包含的HashEntry元素的個數超過本變量值時,觸發table的擴容
transient int threshold;
//裝載因子
final float loadFactor;
- ConcurrentHashMap.HashEntry
HashEntry封裝了key-value對,是一個單向鏈表結構,每個HashEntry節點都維護著next HashEntry節點的引用。
static final class HashEntry<K,V>
final int hash;
final K key;
volatile V value;
//HashEntry鏈表中的下一個entry。PS:JDK 7中該字段不是final的,意味著該字段可修改,而且也確實在remove方法中對該地段進行了修改
volatile HashEntry<K,V> next;
構造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
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;
// create segments and segments[0]
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;
}
a) 限制并發等級最大為MAX_SEGMENTS,即2^16。
b) 計算真實的并發等級ssize,必須是2的N次方,即 ssize( actual_concurrency_level ) >= concurrency_level。
舉例:concurrencyLevel等于14,15或16,ssize都會等于16,即容器里鎖的個數也是16。
Q:為什么數組的長度都需要設計成2^N次方了?
A:這是因為元素在數組中的定位主要是通過H(key) & (數組長度 - 1)方式實現的,這樣我們稱(數組長度 - 1)為element_mask。那么假設有一個長度為16和長度為15的數組,他們element_mask分別為15和14。即array_16_element_mask = 15(二進制”1111”);array_15_element_mask = 14(二進制”1110”)。你會發現所以和”1110”進行與操作結果的最后一位都是0,這就導致數組的’0001’、’0011’、’1001’、’0101’、’1101’、’0111’位置都無法存放數據,這就導致了數組空間的浪費,以及數據沒有得到更好的分散。而使用array_16_element_mask = 15(二進制”1111”)則不會有該問題,數據可以分散到數組個每個索引位置。
c) sshift表示在通過H(key)來定位segment的index時,參與到segmentMask掩碼與運算的H(key)高位位數。
d) 計算每個Segment中HashEntry[]數組的長度,根據數據均勻分配到各個segment的HashEntry[]中,并且數組長度必須是2的N次方的思路來獲取。注意,HashEntry[]數組的長度最小為2。
e) 創建一個Segment對象,將新建的Segment對象放入Segment[]數組中index為0的位置。這里只先構建了Segnemt[]數組的一個元素,則其他index的元素在被使用到時通過ensureSegment(index)方法來構建。
重要方法
- segment的定位
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u))
通過key的二次哈希運算后再進行移位和與運算得到key在segment[]數組中所對應的segment
a) hash(key)
private int hash(Object k) {
int h = hashSeed;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// 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);
}
??這里之所以需要將key.hashCode再進行一次hash計算,是為了減少哈希沖突,使元素能夠均勻的分布在不同的Segment上,從而提高容器的存取效率。
b) 取hash(key)結果的(32 - segmentShift)位數的高位和segmentMask掩碼進行與運算。(其實,與運算時,就是“hash(key)的高segmentMask(十進制值)位"于“segmentMask的二進制值”進行與操作,此時進行與操作的兩個數的有效二進制位數是一樣的了。)
c) 將b)的結果j進行 (j << SSHIFT) + SBASE 以得到key在segement[]數組中的位置
舉例:假如哈希的質量差到極點,那么所有的元素都在一個Segment中,不僅存取元素緩慢,分段鎖也會失去意義。我做了一個測試,不通過再哈希而直接執行哈希計算。
System.out.println(Integer.parseInt("0001111", 2) & 15);
System.out.println(Integer.parseInt("0011111", 2) & 15);
System.out.println(Integer.parseInt("0111111", 2) & 15);
System.out.println(Integer.parseInt("1111111", 2) & 15);
計算后輸出的哈希值全是15,通過這個例子可以發現如果不進行再哈希,哈希沖突會非常嚴重,因為只要低位一樣,無論高位是什么數,其哈希值總是一樣。我們再把上面的二進制數據進行再哈希后結果如下,為了方便閱讀,不足32位的高位補了0,每隔四位用豎線分割下。
0100 | 0111 | 0110 | 0111 | 1101 | 1010 | 0100 | 1110 |
---|---|---|---|---|---|---|---|
1111 | 0111 | 0100 | 0011 | 0000 | 0001 | 1011 | 1000 |
0111 | 0111 | 0110 | 1001 | 0100 | 0110 | 0011 | 1110 |
1000 | 0011 | 0000 | 0000 | 1100 | 1000 | 0001 | 1010 |
可以發現每一位的數據都散列開了,通過這種再哈希能讓數字的每一位都能參加到哈希運算當中,從而減少哈希沖突。
- HashEntry定位
int h = hash(key);
((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)
主要通過對key進行二次hash運算,再講哈希結果和HashEntry[]的長度掩碼進行與運算得到key所對應的HashEntry在數組中的索引。HashEntry的定位和Segment的定位方式很像,但是HashEntry少了將hash(key)的結果進行掩碼取高位后再與數組長度與操作,而是直接將hash(key)的結果和數組長度的掩碼進行與操作。其目的是避免兩次哈希后的值一樣,導致元素雖然在Segment里散列開了,但是卻沒有在HashEntry里散列開( 也就是說,如果Segment和HashEntry的定位方式一樣,那么到同一個Segment的key都會落到該segment中的同一個HashEntry了 )。
Unsafe類中的putOrderedObject、getObjectVolatile方法
getObjectVolatile:使用volatile讀的語義獲取數據,也就是通過getObjectVolatile獲取數據時回去主存中獲取最新的數據放到線程的緩存中,這能保證正確的獲取最新的數據。
putOrderedObject:為了控制特定條件下的指令重排序和內存可見性問題,Java編譯器使用一種叫內存屏障(Memory Barrier,或叫做內存柵欄,Memory Fence)的CPU指令來禁止指令重排序。java中volatile寫入使用了內存屏障中的LoadStore屏障規則,對于這樣的語句Load1; LoadStore; Store2,在Store2及后續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。volatile的寫所插入的storeLoad是一個耗時的操作,因此出現了一個對volatile寫的升級版本,利用lazySet方法進行性能優化,在實現上對volatile的寫只會在之前插入StoreStore屏障,對于這樣的語句Store1; StoreStore; Store2,在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見,也就是按順序的寫入。UNSAFE.putOrderedObject正是提供了這樣的語義,避免了寫寫指令重排序,但不保證內存可見性,因此讀取時需借助volatile讀保證可見性。ensureSegment(k)
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
根據計算得到的index從segment[]數組中獲取segment,如果segment不存在,則創建一個segment并通過CAS算法放入segment[]數組中。這里的獲取和插入分別通過UNSAGE.getObjectVolatile(??保證獲取segment[]最新數據)和UNSAFE.cmpareAndSwapObject(??保證原子性的將新建的segment插入segment[]數組,并使其他線程可見)實現,并不直接對segment[]數組操作。
- HashEntry<K,V> scanAndLockForPut(K key, int hash, V value)
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
在put操作嘗試加鎖沒成功時,不是直接進入等待狀態,而是調用??scanAndLockForPut()方法,該方法實現了:
a) 首次進入該方法,重試次數retries初始值為-1。
b) 若retries為-1,則判斷查詢key對應的HashEntry節點鏈中是否已經存在了該節點,如果還沒則預先創建一個新節點。然后將retries=0;
c) 然后嘗試MAX_SCAN_RETRIES次獲取鎖( 自旋鎖 ),如果依舊沒能成功獲得鎖,則進入等待狀態(互斥鎖)。
JDK7嘗試使用自旋鎖來提升性能,好處在于:自旋鎖當前的線程不會掛起,而是一直處于running狀態,這樣一旦能夠獲得鎖時就key在不進行上下文切換的情況下獲取到鎖。
d) 如果在嘗試MAX_SCAN_RETRIES次獲取鎖的過程,key對應的entry發送了變化,則將嘗試次數重置為-1,從第b)步驟重新開始
- void scanAndLock(Object key, int hash)
private void scanAndLock(Object key, int hash) {
// similar to but simpler than scanAndLockForPut
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
int retries = -1;
while (!tryLock()) {
HashEntry<K,V> f;
if (retries < 0) {
if (e == null || key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
}
在replace、remove操作嘗試加鎖沒成功時,不是直接進入等待狀態,而是調用??scanAndLock()方法。該方法是實現和scanAndLockForPut()差不了多少,主要的區別在于scanAndLockForPut()方法在key對應entry不存在時是不會去創建一個HashEntry對象的。
- V get(Object key)
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;
}
在JDK 7中get的實現原理已經和JDK 6不同了,JDK 6通過volatile實現多線程間內存的可見性。而JDK 7為了提升性能,用UNSAFE.getObjectVolatile(...)來獲取segment[]數組和HashEntry[]數組中對應index的最新值。同時值得說明的是,當volatile引用一個數組時,數組中的元素是不具有volatile特性的,所以,也需要通過UNSAFE.getObjectVolatile(…)來獲取數組中真實的數據。
- put操作
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
a) 通過key算出對應的segment在segment[]中的位置,如果對應的segment不存在,則創建。
b) 將key、value插入到segment中對應的HashEntry中
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;
}
a) 嘗試獲得鎖,如果失敗,則調用scanAndLockForPut(...)通過自旋等待的方式獲得鎖。注意,這里鎖操作鎖的只是當前這個segment,而不會影響segment[]數組中其他的segment對象的寫操作。這是ConcurrentHashMap實現并發寫操作的精髓所在。通過分段鎖來支持一定并發量的寫操作,并通過volatile以及UNSAFE.getObjectVolatile、UNSAFE.putOrderedObject來實現不加鎖的讀操作,也就是支持任何并發量的讀操作。
b) 計算key應插入的HashEntry在HashEntry[]數組的index,并通過UNSAFE.getObjectVolatile(...)方式獲取最新的到HashEntry對象
c) 判斷HashEntry鏈中是否已經存在該key了,如果存在則將key的value替換成新值,并將modCount加1
d) 如果HashEntry鏈中不存在該key,則將key-value出入到HashEntry鏈頭處,并將count加1,但此時count還未更新到segment中。
e) 如果在count加1后發現目前HashEntry鏈表長度以及達到了閾值并且HashEntry的鏈表長度小于限制的最大長度,則會進行HashEntry的擴容操作。注意,在JDK 7中是確定當前put操作是會加入一個新節點情況下才會觸發擴容操作,而在JDK 6中,可能存在put操作只是替換一個已經存在的key的value值的情況下也會觸發擴容操作。
f) 如果count加1未觸發閾值,則通過UNSAFE.putOrderedObject(…)方式將最新的HashEntry更新到HashEntry[]數組中。
g) 更新segment中的modCount、count值
h) 釋放鎖。釋放鎖的操作會將當前線程緩存里的數據寫到主存中。
- rehash(HashEntry<K,V> node)
private void rehash(HashEntry<K,V> node) {
/*
* Reclassify nodes in each list to new table. Because we
* are using power-of-two expansion, the elements from
* each bin must either stay at same index, or move with a
* power of two offset. We eliminate unnecessary node
* creation by catching cases where old nodes can be
* reused because their next fields won't change.
* Statistically, at the default threshold, only about
* one-sixth of them need cloning when a table
* doubles. The nodes they replace will be garbage
* collectable as soon as they are no longer referenced by
* any reader thread that may be in the midst of
* concurrently traversing table. Entry accesses use plain
* array indexing because they are followed by volatile
* table write.
*/
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
當HashEntry的數量達到閾值時就會觸發HashEntry[]數組的擴容操作
a) 創建new HashEntry[]數組,new HashEntry[]數組的容量為old HashEntry的2倍
b) 設置新的閾值
c) 將old HashEntry[]數組中的內容放入new HashEntry[]中,這并不是盲目的將元素一一取出然后計算元素在new HashEntry的位置,然后插入。這里Doug Lea做了一些優化。
如果old HashEntry[]數組的元素HashEntry鏈表,若該HashEntry鏈表的頭節點不存在next節點,即說明該HashEntry鏈表是個單節點,則直接將HashEntry插入到new HashEntry[]數組對應的位置中。
因為new HashEntry[]的length是old HashEntry[]的2倍,所以對應的new sizeMask比old sizeMask多了old HashEntry[] length的大小( 比如,old_HashEntry_array_length為8,則old sizeMask為’0000 0111’;new_HashEntry_array_length為16,則new sizeMask為’0000 1111’)。所以元素在new HashEntry[]的new index要么和old index一樣,要么就是old_index + old_HashEntry_array_length。因此我們可通過對節點的復用來減少不必要的節點創建,通過計算每個HashEntry鏈表中每個entry的new index值,如果存在從某個entry開始到該HashEntry鏈表末尾的所有entrys,它們的new index值都一樣,那么就該entry直接插入到new HashEntry[newIndex]中,當然最壞的請求就是該entry就是HashEntry鏈的最后一個entry。然后只需重建HashEntry中該entry之前的到鏈表頭的entry節點,分別將新構建的entry插入到new HashEntry[]中。
再者,經統計,在使用默認閾值的情況下,一般只有1/6的節點需要重新構建最后將當前操作新構建的節點加入到new HashEntry[]數組中
d) old HashEntry如果沒有其他讀線程操作引用時,將會盡快被垃圾回收。
e) 擴容操作因為要重新構建正整個HashEntry[]數組,所以不需要通過UNSAFE.putOrderedObject(...)方式將元素插入一個已經存在的HashEntry[]中,而是直接通過索引操作插入到new HashEntry[]數組就好,最后我們會將new HashEntry[]直接賦值給volatile tables字段,這樣就可以保證new HashEntry[]對其他線程可見了remove操作
public V remove(Object key) {
int hash = hash(key);
Segment<K,V> s = segmentForHash(hash);
return s == null ? null : s.remove(key, hash, null);
}
a) 根據key計算出該key對應的segment在segment[]數組中的index,并獲取該segment。
b) 將key從該segment中移除
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
a) 嘗試獲得鎖,如果失敗則調用scanAndLock(...)通過自旋等待的方式獲得鎖。
b) 獲取key鎖對應的HashEntry鏈表,并在該HashEntry中找到key對應entry節點
c) 如果key對應的節點是在HashEntry鏈表頭,則直接將key的next節點通過UNSAFE.putOrderedObject的方式這是為對HashEntry[]數組中對應的位置,即使得next節點稱為成為鏈表頭。
d) 如果key不是HashEntry的鏈表頭節點,則將key的前一個節點的next節點修改為key的next節點。額,這么說太繞了,舉個例子吧~
key對應的節點:current_HashEntry;current_HashEntry的前一個節點:pre_HashEntry;current_HashEntry的下一個節點:next_HashEntry
刪除前:
pre_HashEntry.next ——> current_HashEntry
current_HashEntry.next ——> next_HashEntry
刪除后:
pre_HashEntry.next ——> next_HashEntry
e) 修改segment屬性:modCount加1,count減1
f) 釋放鎖
- size()
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;
}
a) 會先嘗試RETRIES_BEFORE_LOCK次( 即2次 )不加鎖的情況下,將segment[]數組中每個segment的count累加,同時也會將每個segment的modCount進行累加。如果兩次不加鎖的操作后,modCountSum值是一樣的,這說明在這兩次累加segmentcount的過程中ConcurrentHashMap沒有發生結構性變化,那么就直接返回累加的count值
b) 如果在兩次累加segment的count操作期間ConcurrentHashMap發生了結構性改變,則會通過將所有的segment都加鎖,然后重新進行count的累加操作。在完成count的累加操作后,釋放所有的鎖。最后返回累加的count值。
c) 注意,如果累加的count值大于了Integer.MAX_VALUE,則返回Integer.MAX_VALUE。
弱一致性
相對于HashMap的fast-fail,ConcurrentHashMap的迭代器并不會拋出ConcurrentModificationException異常。這是由于ConcurrentHashMap的讀行為是弱一致性的。
也就是說,在同時對一個segment進行讀線程和寫線程操作時,并不保證寫操作的行為能被并行允許的讀線程所感知。
比如,當一個寫線程和讀線程并發的對同一個key進行操作時:寫線程在操作一個put操作,如果這個時候put的是一個已經存在的key值,則會替換該key對應的value值,因為value是volatile屬性的,所以該替換操作時能立即被讀線程感知的。但如果此時的put是要新插入一個entry,則存在兩種情況:①在寫線程通過UNSAFE.putOrderedObject方式將新entry插入到HashEntry鏈表后,讀線程才通過UNSAFE.getObjectVolatile來獲取對應的HashEntry鏈表,那么這個時候讀線程是能夠獲取到這個新插入的entry的;②反之,如果讀線程的UNSAFE.getObjectVolatile操作在寫線程的UNSAFE.putOrderedObject之前,則就無法感知到這個新加入的entry了。
其實在大多數并發的業務邏輯下,我們是允許這樣的弱一致性存在的。如果你的業務邏輯不允許這樣的弱一致性存在的,你可以考慮對segment中的HashEntry鏈表的讀操作加鎖,或者將segment改造成讀寫鎖模式。但這都將大大降低ConcurrentHashMap的性能并且使得你的程序變得復雜且難以維護。或許你該考慮使用其他的存儲模型代替ConcurrentHashMap。
后記
雖然JDK 8已經出來很久了,但是我還是花了很多時間在JDK 7的ConcurrentHashMap上,一個很重要的原因是,我認為ConcurrentHashMap在并發模式下的設計思想是很值得我們深究和學習的,無論是jdk7相對于jdk6的各種細節和性能上的優化,還是jdk8的大改造都是對并發編程各種模式很好的學習。文章還有很多可以繼續深入挖掘的點,希望在后期的學習中能夠繼續完善~
參考
http://www.infoq.com/cn/articles/ConcurrentHashMap/
http://www.blogjava.net/DLevin/archive/2013/10/18/405030.html
https://my.oschina.net/7001/blog/896587