瞭解ConcurrentHashMap 實現原理,建議首先了解下HashMap實現原理。
爲什麼要用ConcurrentHashMap
HashMap線程不安全,而Hashtable是線程安全,但是它使用了synchronized進行方法同步,插入、讀取數據都使用了synchronized,當插入數據的時候不能進行讀取(相當於把整個Hashtable都鎖住了,全表鎖),當多線程併發的情況下,都要競爭同一把鎖,導致效率極其低下。而在JDK1.5後爲了改進Hashtable的痛點,ConcurrentHashMap應運而生。
ConcurrentHashMap爲什麼高效?
JDK1.5中的實現
ConcurrentHashMap使用的是分段鎖技術,將ConcurrentHashMap將鎖一段一段的存儲,然後給每一段數據配一把鎖(segment),當一個線程佔用一把鎖(segment)訪問其中一段數據的時候,其他段的數據也能被其它的線程訪問,默認分配16個segment。默認比Hashtable效率提高16倍。
ConcurrentHashMap的結構圖如下(網友貢獻的圖,哈):
JDK1.8中的實現
ConcurrentHashMap取消了segment分段鎖,而採用node節點+CAS和synchronized來保證併發安全。數據結構跟HashMap1.8的結構一樣,Node數組+鏈表/紅黑二叉樹。
synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提升N倍。
JDK1.8的ConcurrentHashMap的結構圖如下:
下面的圖更直接:
ConcurrentHashMap 源碼分析
只有在執行第一次put
方法時纔會調用initTable()
初始化Node
數組,實現如下:
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(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
put實現
當執行put
方法插入數據時,根據key的hash值定位Node[]數組的索引座標,在Node
數組中找到相應的位置,實現如下:
1、如果相應位置的Node
還未初始化,則使用CAS進行添加(鏈表的頭結點),添加失敗則進入下次循環。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
2、如果相應位置的Node
不爲空,且當前該節點不處於移動狀態,則對該節點加synchronized
鎖,如果該節點的hash
不小於0,則遍歷鏈表更新節點或插入新節點;
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
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;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
3、如果該節點是TreeBin
類型的節點,說明是紅黑樹結構,則通過putTreeVal
方法往紅黑樹中插入節點;
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
4、如果binCount
不爲0,說明put
操作對數據產生了影響,如果當前鏈表的個數達到8個,則通過treeifyBin
方法轉化爲紅黑樹,如果oldVal
不爲空,說明是一次更新操作,沒有對元素個數產生影響,則直接返回舊值;
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
5、如果插入的是一個新節點,則執行addCount()
方法嘗試更新元素個數baseCount
;
size實現
1.8中使用一個volatile
類型的變量baseCount
記錄元素的個數,當插入新數據或則刪除數據時,會通過addCount()
方法更新baseCount
,實現如下:
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
1、初始化時counterCells
爲空,在併發量很高時,如果存在兩個線程同時執行CAS
修改baseCount
值,則失敗的線程會繼續執行方法體中的邏輯,使用CounterCell
記錄元素個數的變化;
2、如果CounterCell
數組counterCells
爲空,調用fullAddCount()
方法進行初始化,並插入對應的記錄數,通過CAS
設置cellsBusy字段,只有設置成功的線程才能初始化CounterCell
數組,實現如下:
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
CounterCell[] rs = new CounterCell[2];
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
3、如果通過CAS
設置cellsBusy字段失敗的話,則繼續嘗試通過CAS
修改baseCount
字段,如果修改baseCount
字段成功的話,就退出循環,否則繼續循環插入CounterCell
對象;
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break;
所以在1.8中的size
實現比1.7簡單多,因爲元素個數保存baseCount
中,部分元素的變化個數保存在CounterCell
數組中,實現如下:
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;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
通過累加baseCount
和CounterCell
數組中的數量,即可得到元素的總個數;
分析代碼主要目的:分析是如果利用CAS和Synchronized進行高效的同步更新數據。
下面插入數據源碼:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//ConcurrentHashMap 不允許插入null鍵,HashMap允許插入一個null鍵
if (key == null || value == null) throw new NullPointerException();
//計算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
//for循環的作用:因爲更新元素是使用CAS機制更新,需要不斷的失敗重試,直到成功爲止。
for (Node<K,V>[] tab = table;;) {
// f:鏈表或紅黑二叉樹頭結點,向鏈表中添加元素時,需要synchronized獲取f的鎖。
Node<K,V> f; int n, i, fh;
//判斷Node[]數組是否初始化,沒有則進行初始化操作
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//通過hash定位Node[]數組的索引座標,是否有Node節點,如果沒有則使用CAS進行添加(鏈表的頭結點),添加失敗則進入下次循環。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//檢查到內部正在移動元素(Node[] 數組擴容)
else if ((fh = f.hash) == MOVED)
//幫助它擴容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//鎖住鏈表或紅黑二叉樹的頭結點
synchronized (f) {
//判斷f是否是鏈表的頭結點
if (tabAt(tab, i) == f) {
//如果fh>=0 是鏈表節點
if (fh >= 0) {
binCount = 1;
//遍歷鏈表所有節點
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果節點存在,則更新value
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;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//TreeBin是紅黑二叉樹節點
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//添加樹節點
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//如果鏈表長度已經達到臨界值8 就需要把鏈表轉換爲樹結構
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//將當前ConcurrentHashMap的size數量+1
addCount(1L, binCount);
return null;
}
- 判斷Node[]數組是否初始化,沒有則進行初始化操作
- 通過hash定位Node[]數組的索引座標,是否有Node節點,如果沒有則使用CAS進行添加(鏈表的頭結點),添加失敗則進入下次循環。
- 檢查到內部正在擴容,如果正在擴容,就幫助它一塊擴容。
- 如果f!=null,則使用synchronized鎖住f元素(鏈表/紅黑二叉樹的頭元素)
4.1 如果是Node(鏈表結構)則執行鏈表的添加操作。
4.2 如果是TreeNode(樹型結果)則執行樹添加操作。 - 判斷鏈表長度已經達到臨界值8 就需要把鏈表轉換爲樹結構。
總結:
JDK8中的實現也是鎖分離的思想,它把鎖分的比segment(JDK1.5)更細一些,只要hash不衝突,就不會出現併發獲得鎖的情況。它首先使用無鎖操作CAS插入頭結點,如果插入失敗,說明已經有別的線程插入頭結點了,再次循環進行操作。如果頭結點已經存在,則通過synchronized獲得頭結點鎖,進行後續的操作。性能比segment分段鎖又再次提升。