1.本文簡介
1.1 ConcurrentHashMap put方法介紹
2.源碼解讀
2.1 put方法解讀
調用put(K key, V value)方法實際調用的是putVal(key, value, false),如下:
public V put(K key, V value) {
return putVal(key, value, false);
}
所以,解讀重點在putVal方法上。先看一下putVal方法操作主要流程:
1.若 key == null || value == null,拋出 NPE,結束;否則,計算key的hash值,繼續往下執行;
2.對存放<key,value>的 Node<K,V>[] tab 數組進行for循環遍歷,for (Node<K,V>[] tab = table;;),for循環退出交由執行體控制;
執行體進行如下一系列判斷:
2.1 判斷tab是否初始化,未初始化則進行初始化(首次初始化默認數組長度爲16);
if (tab == null || (n = tab.length) == 0)
tab = initTable();
2.2 判斷key對應的數組位置上是否爲null,若尚未發生hash碰撞,即進行CAS操作,new 一個 Node<K,V>存放到tab中,退出for循環;
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.3 判斷tab是否需要進行擴容:
else if ((fh = f.hash) == MOVED) // MOVED = -1
tab = helpTransfer(tab, f);
2.4 以上判斷都未進入,說明tab已初始化,且有容量存放數據,但發生了hash衝突,接下來則進入hash衝突解決階段:
先用synchronized 鎖住f,即tab數組發生hash衝突位置上的元素對象Node,然後再判斷一下tabAt(tab, i) == f,主要作用是tab是一個volatile修飾的對所有線程可見的共享變量,在synchronized 上鎖成功之前其他線程有可能對tab做了操作,如擴容,remove等。
2.4.1 if (fh >= 0) 當做鏈表處理:進行for循環,若hash 和 key 都相同,則用新值替換舊值,break;否則一直循環到鏈表尾,當 (e = e.next) == null 時,pred.next = new Node<K,V>,添加數據到鏈表,break; 注意此處 ++binCount,binCount用來統計鏈表長度;
2.4.2 else if (f instanceof TreeBin) 當做樹結構處理:若添加的key不存在於樹中,則 putTreeVal 方法直接添加並返回null,若是key對應的數據已存在,putTreeVal 方法返回樹中的Node<K,V> p,並用新值替換舊值;
2.4.3 在synchronized 代碼塊執行結束後,判斷鏈表長度是否 >= 閾值 8(TREEIFY_THRESHOLD),是則轉爲紅黑樹;
之後要麼 return oldVal ,要麼break 結束for循環;
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;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { // key 存在,更新
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); //key 不存在,鏈表中追加新元素
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//key不存在則putTreeVal方法直接添加新元素並返回null,key存在則返回對應節點p並做val更新
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// TREEIFY_THRESHOLD 默認8 鏈表轉紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
3. for 循環處理結束後,檢查tab是否需要擴容:
addCount(1L, binCount); //檢查擴容
4. putVal 方法執行結束: return null;
2.2 put 方法流程圖
爲更直觀瞭解put方法的執行過程,畫個流程圖:
2.3 細節探討
2.3.1 鏈表轉紅黑樹
1. 鏈表轉紅黑樹閾值 TREEIFY_THRESHOLD 8 的問題,看源碼,是用binCount來比較;但binCount並不能完全正確的及時反映鏈表的長度。看下面這段代碼:
synchronized (f) {
if (tabAt(tab, i) == f) {
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;
}
}
}
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) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
(1)假設本次key存在,且在鏈表的中間位置,那麼break的時候 binCount 不能代表鏈表的實際長度,此時實際長度可能已達 到8;
(2)假設本次key不存在,且鏈表長度已經爲7,那麼添加新元素後,實際鏈表長度已經是8,但break時,binCount 還是7;
(3)至於閾值爲什麼爲8,哈哈,不清楚哈,估計是基於測試統計給出來的吧。有個 泊松分佈 概率函數,感興趣的可以瞭解 下。
2. 鏈表轉紅黑樹,關於tab 數組長度的問題。看代碼:
/**
* Replaces all linked nodes in bin at given index unless table is
* too small, in which case resizes instead.
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1); //MIN_TREEIFY_CAPACITY 默認64,tryPresize 擴容
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
//紅黑樹轉化
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
(1)MIN_TREEIFY_CAPACITY 爲64,可見,當鏈表長度達到8但tab長度小於64時,並不會直接進行紅黑樹轉化,而是對tab進行擴容,根據新的new_tab大小以及key的hash值,重新分配index,將原有元素copy到新的new_tab中,且原有元素若爲鏈表,擴容後理論上分爲2個小鏈表,鏈表位置爲 index=i 和 index=i+n ,n 表示原有tab大小;若原有元素爲樹結構,擴容後理論上分爲2個小樹,樹位置爲 index=i 和 index=i+n ,n 表示原有tab大小,若小樹中元素小於等於UNTREEIFY_THRESHOLD 6 ,擴容時會向下轉化爲鏈表,否則繼續生成樹結構;由此也可見,當鏈表長度達到8但tab長度小於64時,採用的是擴容來降低hash衝突,從而理論上降低鏈表長度。
3.小結
3.1 ConcurrentHashMap 1.8 put 方法主要採用 CAS (hash未衝突使用) + synchronized (hash衝突使用),來解決併發時的線程安全問題;
3.2 鏈表轉爲紅黑樹,必要條件是 數組長度不小於64,且鏈表長度不小於8;
4.附完整源碼
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*
* <p>The value can be retrieved by calling the {@code get} method
* with a key that is equal to the original key.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key or value is null
*/
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) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
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
}
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;
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;
}
}
}
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) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
評論區歡迎討論!覺得有用點個贊再走吧。感謝!!!