本文主要從原始碼層次探討 ConcurrentHashMap 的並發控制實現,全文可以濃縮為下面的內容,也可以用來回答問題《ConcurrentHashMap 是如何實現執行緒安全支持高並發的?》
在 JDK 7 中,ConcurrentHashMap 是使用陣列加鏈表的結構實現的。陣列又分為 HashEntry 陣列和 Segment 陣列。其中 Segment 陣列使用 ReentrantLock 控制並發,HashEntry 就是存放鍵值對的。在 JDK 7 中為每一段桶分配一個 segment 分段鎖,當需要操作某一個桶中的資料時,需要先取得對應的分段鎖。
在 JDK 8 中,ConcurrentHashMap 是使用的陣列加鏈表加紅黑樹實現的。透過 volatile + cas (樂觀鎖)和 synchronized (悲觀鎖)實現並發控制的。鎖的粒度減少到對桶頭加鎖,並發度更高。並且鏈表可以優化為紅黑樹,查詢效率也比 JDK 7 的高;其次 HashMap 擴容的時候,其他執行緒可以協助擴容。
ConcurrentHashMap 的重要屬性和內部類
// 默認為 0
// 當初始化時, 為 -1
// 當擴容時, 為 -(1 + 擴容執行緒數)
// 當初始化或擴容完成後,為下一次的擴容的閾值大小
private transient volatile int sizeCtl;
// 整個 ConcurrentHashMap 就是 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
// hash 表
transient volatile Node<K,V>[] table;
// 擴容時的新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 擴容時如果某個 bin 遷移完畢, 用 ForwardingNode 作為舊 table bin 的頭結點
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 時, 用來佔位, 計算完成後替換為普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作為 treebin 的頭節點, 存儲 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作為 treebin 的節點, 存儲 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
可以發現在 JDK 8 的 ConcurrentHashMap 中,底層陣列就是 Node[],存放的都是 Node 或者它的子類。相對於 HashMap 來說,ConcurrentHashMap 在陣列中用 ForwardingNode 和 TreeBin 當哨兵。
sizeCtl 的作用
ForwardingNode 的哨兵作用:
擴容機制
樹化機制
TREEIFY_THRESHOLD
,常量值 8)時,滿足第一條件table.length ≥ MIN_TREEIFY_CAPACITY
(常量值 64)時,才真正將鏈表樹化;否則不樹化,而是先觸發一次擴容(resize),讓更多的桶分攤這些節點ConcurrentHashMap 為什麼要加容量檢查?
對於小表,鏈表查找開銷尚小,擴容更有利於分散節點;等表足夠大,才值得投入樹化的維護成本。
HashMap 和 ConcurrentHashMap 擴容機制和樹化的總結:
// 獲取 Node[] 中第 i 個 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {}
// cas 修改 Node[] 中第 i 個 Node 的值, c 為舊值, v 為新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {}
// 直接修改 Node[] 中第 i 個 Node 的值, v 為新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {}
可以看到 ConcurrentHashMap 使用的是懶惰初始化,在構造方法中僅僅計算了 table 的大小,以後在第一次使用時才會真正創建。
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// tableSizeFor 仍然是保證計算的大小是 2^n, 即 16,32,64 ...
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get 實現了無鎖讀取,但是並不保證一致性讀取。ForwardingNode 發布前讀舊桶,發布後讀新表。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread 把用戶的 hashCode() 做一次高低位混洗,降低糟糕哈希(低位全 0)的碰撞概率;並保證結果是正數(配合位運算定位桶位)
int h = spread(key.hashCode());
// tabAt(...) 不是普通陣列讀,而是經由 Unsafe 的帶內存語義的讀取,保證是 happens‑before 且不加鎖獲取
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果頭結點已經是要查找的 key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
// 桶頭命中 O(1) 的時間複雜度
return e.val;
}
// hash 為負數表示該 bin 在擴容中或是 treebin, 這時調用 find 方法來查找
else if (eh < 0)
// find 函數會處理正在進行遷移的場景 以及 正常紅黑樹的場景
return (p = e.find(h, key)) != null ? p.val : null;
// 正常遍歷鏈表, 用 equals 比較
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get 方法的源碼分析:首先判斷桶頭是否符合、再判斷是否為特殊桶(紅黑樹或者是 ForwardingNode)、走到這裡就說明是鏈表判斷了,直接循環遍歷判斷。
● 如果 table 不為空且長度大於 0 且索引位置有元素
○ if 頭節點 key 的 hash 值相等
■ 頭節點的 key 指向同一地址或者 equals
● 返回 value
○ else if 頭節點的 hash 為負數(bin 在擴容或者是 treebin)
■ 調用 find 方法查找
○ 走到這裡說明是正常鏈表遍歷,進入循環(e 不為空)
■ 節點 key 的 hash 值相等,且 key 指向同一地址或 equals
● 返回 value
● 返回 null
以下陣列簡稱(table),鏈表簡稱(bin)。
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 其中 spread 方法會綜合高位低位, 具有更好的 hash 性
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
// f 是鏈表頭節點
// fh 是鏈表頭結點的 hash
// i 是鏈表在 table 中的下標
Node<K,V> f; int n, i, fh;
// 要創建 table
if (tab == null || (n = tab.length) == 0)
// 初始化 table 使用了 cas, 無需 synchronized 創建成功, 進入下一輪循環
tab = initTable();
// 要創建鏈表頭節點
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 添加鏈表頭使用了 cas, 無需 synchronized
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 幫忙擴容
else if ((fh = f.hash) == MOVED)
// 幫忙之後, 進入下一輪循環
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 鎖住鏈表頭節點
synchronized (f) {
// 再次確認鏈表頭節點沒有被移動
if (tabAt(tab, i) == f) {
// 鏈表
if (fh >= 0) {
binCount = 1;
// 遍歷鏈表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同的 key (更新機制)
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 更新
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 已經是最後的節點了, 新增 Node, 追加至鏈表尾 (插入機制)
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 紅黑樹
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// putTreeVal 會看 key 是否已經在樹中, 是, 則返回對應的 TreeNode
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
// 釋放鏈表頭節點的鎖
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果鏈表長度 >= 樹化閾值(8), 進行鏈表轉為紅黑樹
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加 size 計數
addCount(1L, binCount);
return null;
}
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield();
// 嘗試將 sizeCtl 設置為 -1(表示初始化 table)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 獲得鎖, 創建 table, 這時其它執行緒會在 while() 循環中 yield 直至 table 創建
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
// check 是之前 binCount 的個數 (addCount函數作用不僅是更新Map的Size數量,還能進行擴容的判斷!)
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if (
// 已經有了 counterCells, 向 cell 累加
(as = counterCells) != null ||
// 還沒有, 向 baseCount 累加
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (
// 還沒有 counterCells
as == null || (m = as.length - 1) < 0 ||
// 還沒有 cell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// cell cas 增加計數失敗
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
) {
// 創建累加單元陣列和cell, 累加重試
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
// 獲取元素個數
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// newtable 已經創建了,幫忙擴容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 需要擴容,這時 newtable 未創建
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
先 spread 之後進行取桶 → 若 HashMap 未初始化,則執行 initTable(CAS 抢令牌) → 陣列當前的桶空,就用 CAS 放頭結點 → 桶在遷移則幫助遷移 helpTransfer → 否則對 bin 頭加鎖,鏈表插尾或樹插 → 可能 treeify(但容量小先擴容)。
用分段計數 addCount 增量並透過 sizeCtl 觸發或協助擴容(ForwardingNode + 協作搬桶)。
ConcurrentHashMap使用這兩種手段來保證執行緒安全主要是一種權衡的考量,在某些操作中使用synchronized,還是使用CAS,主要是根據鎖競爭程度來判斷的。
比如:在putVal中,如果計算出來的hash槽沒有存放元素,那麼就可以直接使用CAS來進行設置值,這是因為在設置元素的時候,因為hash值經過了各種擾動後,造成hash碰撞的機率較低,那麼我們可以預測使用較少的自旋來完成具體的hash落槽操作。
當發生了hash碰撞的時候說明容量不夠用了或者已經有大量執行緒訪問了,因此這時使用synchronized來處理hash碰撞比CAS效率要高,因為發生了hash碰撞大概率來說是執行緒競爭比較強烈。
size 計算實際發生在 put,remove 改變集合元素的操作之中。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
// 將 baseCount 計數與所有 cell 計數累加
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
Java 8 陣列(Node) +(鏈表 Node | 紅黑樹 TreeNode ) 。JDK 8 的 ConcurrentHashMap 中,底層陣列就是 Node[],存放的都是 Node 或者它的子類,在陣列中用 ForwardingNode 和 TreeBin 當哨兵,用 sizeCtl 標識擴容狀態。
以下陣列稱之為(table),鏈表簡稱(bin)
它維護了一個 segment 陣列,每個 segment 對應一把鎖(就是繼承 ReentrantLock)。
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;
// ssize 必須是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 陣列的大小
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift 默認是 32 - 4 = 28
this.segmentShift = 32 - sshift;
// segmentMask 默認是 15 即 0000 0000 0000 1111
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;
// 創建 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;
}
構造完成,如下圖所示
JDK 7 的 ConcurrentHashMap 沒有實現懶惰初始化,所以空間佔用相對來說不是很友好。
其中this.segmentShift
和this.segmentMask
的作用是決定將 key 的 hash 結果匹配到哪個 segment。
例如:根據某一 hash 值求 segment 位置,先將高位向低位移動 this.segmentShift 位。
結果再與 this.segmentMask 做位於運算,最終得到 1010 即下標為 10 的 segment。
當第一次創建 Segment 的時候,需要保證安全性。JDK 7 使用的是 CAS 來確保創建 segement 是安全的。
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 計算出 segment 下標
int j = (hash >>> segmentShift) & segmentMask;
// 獲得 segment 對象, 判斷是否為 null, 是則創建該 segment
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null) {
// 這時不能確定是否真的為 null, 因為其它執行緒也發現該 segment 為 null,
// 因此在 ensureSegment 里用 cas 方式保證該 segment 安全性
s = ensureSegment(j);
}
// 進入 segment 的 put 流程
return s.put(key, hash, value, false);
}
segment 繼承了可重入鎖(ReentrantLock),它的 put 方法為:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 嘗試加鎖
HashEntry<K,V> node = tryLock() ? null :
// 如果不成功, 進入 scanAndLockForPut 流程
// 如果是多核 cpu 最多 tryLock 64 次, 進入 lock 流程
// 在嘗試期間, 還可以順便看該節點在鏈表中有沒有, 如果沒有順便創建出來
scanAndLockForPut(key, hash, value);
// 執行到這裡 segment 已經被成功加鎖, 可以安全執行
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 {
// 新增
// 1) 之前等待鎖時, node 已經被創建, next 指向鏈表頭
if (node != null)
node.setNext(first);
else
// 2) 創建新 node
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 3) 擴容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// 將 node 作為鏈表頭
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
發生在 put 中,因為此時已經獲得了鎖,因此 rehash 時不需要考慮執行緒安全。
private void rehash(HashEntry<K,V> node) {
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;
// 過一遍鏈表, 將盡可能把 rehash 後 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;
// 剩餘節點需要新建
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;
// 替換為新的 HashEntry table
table = newTable;
}
附,調試代碼:
public static void main(String[] args) {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 1000; i++) {
int hash = hash(i);
int segmentIndex = (hash >>> 28) & 15;
if (segmentIndex == 4 && hash % 8 == 2) {
System.out.println(i + "\t" + segmentIndex + "\t" + hash % 2 + "\t" + hash % 4 +
"\t" + hash % 8);
}
}
map.put(1, "value");
map.put(15, "value"); // 2 擴容為 4 15 的 hash%8 與其他不同
map.put(169, "value");
map.put(197, "value"); // 4 擴容為 8
map.put(341, "value");
map.put(484, "value");
map.put(545, "value"); // 8 擴容為 16
map.put(912, "value");
map.put(941, "value");
System.out.println("ok");
}
private static int hash(Object k) {
int h = 0;
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);
int v = h ^ (h >>> 16);
return v;
}
get 時並未加鎖,用了 UNSAFE 方法保證了可見性。擴容過程中,get 先發生就從舊表取內容,get 後發生就從新表取內容。
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
// u 為 segment 對象在陣列中的偏移量
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// s 即為 segment
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;
}
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++ ==