前言
ConcurrentHashMap
是一個執行緒安全的HashMap
,主要用於解決HashMap中併發問題。
在ConcurrentHashMap之前,也有執行緒安全的HashMap,比如HashTable
和Collections.synchronizedMap
,但普遍效率低下。
Hashtable效率不高是因為它對資料操作的時候都會透過synchronized
上鎖,也就是我們在講synchronized
說的同步方法。而Collections.synchronizedMap
的效率不高是因為在SynchronizedMap內部維護了一個普通物件Map,還有 排斥鎖mutex,我們在呼叫這個方法的時候就需要傳入一個Map,mutex引數可以傳也可以不傳。建立出synchronizedMap之後,再操作map的時候,就會對這些方法上鎖(如下),也就是我們說的同步程式碼塊,所以效能不高。
因此,才有了JDK1.5引入的ConcurrentHashMap!
ConcurrentHashMap在JDK1.7之前變化不大,在1.8中修改了較多,下面分析一下1.7和1.8中的變化:
- 鎖方面: 由分段鎖(
Segment
繼承自ReentrantLock
)升級為 CAS+synchronized實現; - 資料結構層面: 將
Segment
變為了Node
,減小了鎖粒度,使每個Node
獨立,由原來預設的併發度16變成了每個Node
都獨立,提高了併發度; - hash衝突: 1.7中發生hash衝突採用連結串列儲存,1.8中先使用連結串列儲存,後面滿足條件後會轉換為紅黑樹來最佳化查詢。由於使用了紅黑樹,jdk1.7中連結串列查詢複雜度為O(N),jdk1.8中紅黑樹最佳化為O(logN))。
繼承與實現
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable
可以看到ConcurrentHashMap繼承了AbstractMap
,實現了ConcurrentMap
和Serializable
。
AbstractMap
,這是一個java.util
包下的抽象類,提供Map介面的骨幹實現,以最大限度地減少實現Map這類資料結構時所需的工作量,一般來講,如果需要重複造輪子——自己來實現一個Map,那一般就是繼承AbstractMap。
ConcurrentHashMap
實現了ConcurrentMap
這個介面,ConcurrentMap是在JDK1.5時隨著J.U.C包引入的,這個介面其實就是提供了一些針對Map的原子操作:
package java.util.concurrent;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
public interface ConcurrentMap<K,V> extends Map<K,V> {
//返回指定key對應的值;如果Map不存在該key,則返回defaultValue
default V getOrDefault(Object key, V defaultValue) { ...}
//遍歷Map的所有Entry,並對其進行指定的aciton操作
default void forEach(BiConsumer<? super K, ? super V> action) {...}
//如果Map不存在指定的key,則插入<K,V>;否則,直接返回該key對應的值
V putIfAbsent(K key, V value);
//刪除與<key,value>完全匹配的Entry,並返回true;否則,返回false
boolean remove(Object key, Object value);
//如果存在key,且值和oldValue一致,則更新為newValue,並返回true;否則,返回false
boolean replace(K key, V oldValue, V newValue);
//如果存在key,則更新為value,返回舊value;否則,返回null
V replace(K key, V value);
//遍歷Map的所有Entry,並對其進行指定的funtion操作
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {...}
//如果Map不存在指定的key,則透過mappingFunction計算出value並插入
default V computeIfAbsent(K key , Function<? super K, ? extends V> mappingFunction{...}
//如果Map存在指定的key,則透過mappingFunction計算出value並替換舊值
default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {...}
//根據指定的key,查詢value;然後根據得到的value和remappingFunction重新計算出新值,並替換舊值
default V compute(K key , BiFunction<? super K, ? super V, ? extends V> remappingFunction) {...}
//如果key不存在,則插入value;否則,根據key對應的值和remappingFunction計算出新值,並替換舊值
default V merge(K key, V value , BiFunction<? super V, ? super V, ? extends V> remappingFunction) {...}
而Serializable
則標誌這個類可以進行序列化。
資料結構
ConcurrentHashMap的資料結構對比HashMap要複雜很多,所以在看構造器之前先分析一下它的資料結構。
從上圖可以看出,table一共包含 4 種不同型別的桶,不同的桶用不同顏色表示,分別是Node
、TreeBin
、ForwardingNode
和ReservationNode
這四種結點。
另外,TreeBin
結點所連線的是一顆紅黑樹,紅黑樹結點使用TreeNode
表示,加上前面四種一共是5種結點。
而這裡沒有直接使用TreeNode
的原因是因為紅黑樹的操作比較複雜,包括構建、左旋、右旋、刪除,平衡等操作,用一個代理結TreeBin
來包含這些複雜操作,其實是一種 “職責分離”的思想,另外TreeBin
中也包含了一些加/解鎖操作。
Node結點
- Node是其它四種型別結點的父類;
- 預設連結到table[i],即桶上的結點就是Node結點;
- 當出現hash衝突時,Node結點會首先以連結串列的形式連結到table上,當超過閾值的時候才會轉化為紅黑樹。
/**
* 普通的Entry結點, 以連結串列形式儲存時才會使用, 儲存實際的資料.
*/
static class Node<K, V> implements Map.Entry<K, V> {
final int hash; //透過key計算hash值,透過hash值找相應的桶
final K key;
volatile V val;
volatile Node<K, V> next; // 連結串列指標
Node(int hash, K key, V val, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return val;
}
public final int hashCode() {
return key.hashCode() ^ val.hashCode();
}
public final String toString() {
return key + "=" + val;
}
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u;
Map.Entry<?, ?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?, ?>) o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* 連結串列查詢.
*/
Node<K, V> find(int h, Object k) {
Node<K, V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
TreeNode結點
TreeNode是紅黑樹的結點,TreeNode不會直接連結到桶上面,而是由TreeBin連結,TreeBin會指向紅黑樹的根結點。
/**
* 紅黑樹結點, 儲存實際的資料.
*/
static final class TreeNode<K, V> extends Node<K, V> {
boolean red;
TreeNode<K, V> parent;
TreeNode<K, V> left;
TreeNode<K, V> right;
/**
* prev指標是為了方便刪除.
* 刪除連結串列的非頭結點時,需要知道它的前驅結點才能刪除,所以直接提供一個prev指標
*/
TreeNode<K, V> prev;
TreeNode(int hash, K key, V val, Node<K, V> next,
TreeNode<K, V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K, V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* 以當前結點(this)為根結點,開始遍歷查詢指定key.
*/
final TreeNode<K, V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K, V> p = this;
do {
int ph, dir;
K pk;
TreeNode<K, V> q;
TreeNode<K, V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
TreeBin結點
TreeBin相當於TreeNode的代理結點;TreeBin會直接連結到 table[i] 上,該結點提供了一系列紅黑樹相關的操作,以及加鎖、解鎖操作。
/*
TreeNode的代理結點(相當於封裝了TreeNode的容器,提供針對紅黑樹的轉換操作和鎖控制)
hash值固定為-2
*/
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 紅黑樹結構的根結點
volatile TreeNode<K,V> first; // 連結串列結構的頭結點
volatile Thread waiter; // 最近的一個設定WAITER標識位的執行緒
volatile int lockState; // 整體的鎖狀態標識位,0為初始態
// values for lockState
static final int WRITER = 1; // 二進位制001,紅黑樹的寫鎖狀態
static final int WAITER = 2; // 二進位制010,紅黑樹的等待獲取寫鎖狀態(優先鎖,當有鎖等待,讀就不能增加了)
// 二進位制100,紅黑樹的讀鎖狀態,讀可以併發,每多一個讀執行緒,lockState都加上一個READER值,
static final int READER = 4;
/*
在hashCode相等並且不是Comparable型別時,用此方法判斷大小.
*/
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
return d;
}
// 將以b為頭結點的連結串列轉換為紅黑樹
TreeBin(TreeNode<K,V> b) {...}
// 透過lockState,對紅黑樹的根結點➕寫鎖.
private final void lockRoot() {
if (!U.compareAndSetInt(this, LOCKSTATE, 0, WRITER))
contendedLock(); // offload to separate method ,Possibly blocks awaiting root lock.
}
//釋放寫鎖
private final void unlockRoot() { lockState = 0; }
// 從根結點開始遍歷查詢,找到“相等”的結點就返回它,沒找到就返回null,當存在寫鎖時,以連結串列方式進行查詢,後會面會介紹
final Node<K,V> find(int h, Object k) {... }
/**
* 查詢指定key對應的結點,如果未找到,則直接插入.
* @return 直接插入成功返回null, 替換返回找到的結點的oldVal
*/
final TreeNode<K,V> putTreeVal(int h, K k, V v) {...}
/*
刪除紅黑樹的結點:
1. 紅黑樹規模太小時,返回true,然後進行 樹 -> 連結串列 的轉化,最後刪除;
2. 紅黑樹規模足夠時,不用變換成連結串列,但刪除結點時需要加寫鎖;
*/
final boolean removeTreeNode(TreeNode<K,V> p) {...}
// 以下是紅黑樹的經典操作方法,改編自《演算法導論》
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root , TreeNode<K,V> p) { ...}
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root , TreeNode<K,V> p) {...}
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root , TreeNode<K,V> x) {...}
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) { ... }
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {...} //遞迴檢查紅黑樹的正確性
private static final long LOCKSTATE= U.objectFieldOffset(TreeBin.class, "lockState");
}
ForwardingNode結點
ForwardingNode結點僅僅在 擴容 時才會使用
/**
* ForwardingNode是一種臨時結點,在擴容進行中才會出現,hash值固定為-1,且不儲存實際資料。
* 如果舊table陣列的一個hash桶中全部的結點都遷移到了新table中,則在這個桶中放置一個ForwardingNode,即table[i]=ForwardingNode。
* 讀操作碰到ForwardingNode時,將操作轉發到擴容後的新table陣列上去執行;寫操作碰見它時,則嘗試幫助擴容。
*/
static final class ForwardingNode<K, V> extends Node<K, V> {
final Node<K, V>[] nextTable;
ForwardingNode(Node<K, V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
// 在新的陣列nextTable上進行查詢
Node<K, V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer:
for (Node<K, V>[] tab = nextTable; ; ) {
Node<K, V> e;
int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (; ; ) {
int eh;
K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K, V>) e).nextTable;
continue outer;
} else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
ReservationNode結點
保留結點,ConcurrentHashMap中的一些特殊方法會專門用到該類結點。
/**
* 保留結點.
* hash值固定為-3, 不儲存實際資料
* 只在computeIfAbsent和compute這兩個函式式API中充當佔位符加鎖使用
*/
static final class ReservationNode<K, V> extends Node<K, V> {
ReservationNode() {
super(RESERVED, null, null, null);
}
Node<K, V> find(int h, Object k) {
return null;
}
}
構造器方法
ConcurrentHashMap提供了五個構造器,這五個構造器內部最多也只是計算了下table的初始容量大小,並沒有進行實際的建立table陣列的工作。
因為ConcurrentHashMap用了一種懶載入的模式,只有到首次插入鍵值對的時候,才會真正的去初始化table陣列。
空構造器
public ConcurrentHashMap() {
}
指定table初始容量的構造器
/**
* 指定table初始容量的構造器.
* tableSizeFor會返回大於入參(initialCapacity + (initialCapacity >>> 1) + 1)的最小2次冪值
*/
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
根據已有的Map構造
/**
* 根據已有的Map構造ConcurrentHashMap.
*/
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
指定table初始容量和負載因子的構造器
/**
* 指定table初始容量和負載因子的構造器.
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
指定table初始容量、負載因子、併發級別的構造器
/**
* 指定table初始容量、負載因子、併發級別的構造器.
* <p>
* 注意:concurrencyLevel只是為了相容JDK1.8以前的版本,並不是實際的併發級別,loadFactor也不是實際的負載因子
* 這兩個都失去了原有的意義,僅僅對初始容量有一定的控制作用
*/
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel)
initialCapacity = concurrencyLevel;
long size = (long) (1.0 + (long) initialCapacity / loadFactor);
int cap = (size >= (long) MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int) size);
this.sizeCtl = cap;
}
常量/欄位
原始碼中常量如下:
/**
* 最大容量.
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 預設初始容量
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* 最大陣列長度
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 負載因子,為了相容JDK1.8以前的版本而保留。
* JDK1.8中的ConcurrentHashMap的負載因子恆定為0.75
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 連結串列轉樹的閾值,即連結結點數大於8時, 連結串列轉換為樹.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 樹轉連結串列的閾值,即樹結點樹小於6時,樹轉換為連結串列.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 在連結串列轉變成樹之前,還會有一次判斷:
* 即只有鍵值對數量大於MIN_TREEIFY_CAPACITY,才會發生轉換。
* 這是為了避免在Table建立初期,多個鍵值對恰好被放入了同一個連結串列中而導致不必要的轉化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 在樹轉變成連結串列之前,還會有一次判斷:
* 即只有鍵值對數量小於MIN_TRANSFER_STRIDE,才會發生轉換.
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* 用於在擴容時生成唯一的隨機數.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* 可同時進行擴容操作的最大執行緒數.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int MOVED = -1; // 標識ForwardingNode結點(在擴容時才會出現,不儲存實際資料)
static final int TREEBIN = -2; // 標識紅黑樹的根結點
static final int RESERVED = -3; // 標識ReservationNode結點()
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
/**
* CPU核心數,擴容時使用
*/
static final int NCPU = Runtime.getRuntime().availableProcessors();
原始碼中欄位如下:
/**
* Node陣列,標識整個Map,首次插入元素時建立,大小總是2的冪次.
*/
transient volatile Node<K, V>[] table;
/**
* 擴容後的新Node陣列,只有在擴容時才非空.
*/
private transient volatile Node<K, V>[] nextTable;
/**
* 控制table的初始化和擴容(重要⭐⭐⭐)
* 0 : 初始預設值
* -1 : 有執行緒正在進行table的初始化
* >0 : table初始化時使用的容量,或初始化/擴容完成後的threshold
* -(1 + nThreads) : 記錄正在執行擴容任務的執行緒數
*/
private transient volatile int sizeCtl;
/**
* 擴容時需要用到的一個下標變數.
*/
private transient volatile int transferIndex;
/**
* 計數基值,當沒有執行緒競爭時,計數將加到該變數上。類似於LongAdder的base變數
*/
private transient volatile long baseCount;
/**
* 計數陣列,出現併發衝突時使用。類似於LongAdder的cells陣列
*/
private transient volatile CounterCell[] counterCells;
/**
* 自旋標識位,用於CounterCell[]擴容時使用。類似於LongAdder的cellsBusy變數
*/
private transient volatile int cellsBusy;
// 檢視相關欄位
private transient KeySetView<K, V> keySet;
private transient ValuesView<K, V> values;
private transient EntrySetView<K, V> entrySet;
put()方法
put方法是ConcurrentHashMap類的核心方法
/**
* 插入鍵值對,<K,V>均不能為null.
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
這裡提一嘴,為什麼ConcurrentHashMap和Hashtable的key和value都不允許為null,而HashMap可以呢?
這是因為ConcurrentHashMap和Hashtable都是支援併發的,這樣會有一個問題,當你透過get(k)獲取對應的value時,如果獲取到的是null時,你無法判斷,它是put(k,v)的時候value就為null,還是這個key從來沒有做過對映。
而HashMap是非併發的,可以透過contains(key)來進行校驗。而支援併發的Map在呼叫m.contains(key)和m.get(key)時m可能已經不同了。
put方法內部呼叫了putVal這個私有方法:
/**
* 實際的插入操作
* @param onlyIfAbsent true:僅當key不存在時,才插入
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 再次計算hash值
/**
* 使用連結串列儲存時,binCount記錄table[i]這個桶中所儲存的結點數;
* 使用紅黑樹儲存時,binCount==2,保證put後更改計數值時能夠進行擴容檢查,同時不觸發紅黑樹化操作
*/
int binCount = 0;
for (Node<K, V>[] tab = table; ; ) { // 自旋插入結點,直到成功
Node<K, V> f;
int n, i, fh;
if (tab == null || (n = tab.length) == 0) // CASE1: 首次初始化table —— 懶載入
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // CASE2: table[i]對應的桶為null
// 注意下上面table[i]的索引i的計算方式:[ key的hash值 & (table.length-1) ]
// 這也是table容量必須為2的冪次的原因,讀者可以自己看下當table.length為2的冪次時,(table.length-1)的二進位制形式的特點 —— 全是1
// 配合這種索引計算方式可以實現key的均勻分佈,減少hash衝突
if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null))) // 插入一個連結串列結點
break;
} else if ((fh = f.hash) == MOVED) // CASE3: 發現ForwardingNode結點,說明此時table正在擴容,則嘗試協助資料遷移
tab = helpTransfer(tab, f);
else { // CASE4: 出現hash衝突,也就是table[i]桶中已經有了結點
V oldVal = null;
synchronized (f) { // 鎖住table[i]結點
if (tabAt(tab, i) == f) { // 再判斷一下table[i]是不是第一個結點, 防止其它執行緒的寫修改
if (fh >= 0) { // CASE4.1: table[i]是連結串列結點
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;
}
}
} else if (f instanceof TreeBin) { // CASE4.2: table[i]是紅黑樹結點
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) // 表明本次put操作只是替換了舊值,不用更改計數值
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 計數值加1
return null;
}
putVal這個方法邏輯大概如下:
- 首先根據key計算hash值
(n - 1) & hash
; - 然後透過hash值與table容量進行執行,計算得到key對映到table上的索引;
- 最後加入結點,這裡要略微複雜一些。
putVal()中的四種情況
1.首次初始化table —— 懶載入
之前分析構造器的時候以及put()原始碼的註釋中都說了,ConcurrentHashMap在構造的時候並不會始化table陣列,首次初始化就在這裡透過 initTable()
完成。
在分析initTable()
的原始碼前,我們需要考慮一個問題,如果多個執行緒同時呼叫initTable()
初始化Node陣列怎麼辦?要如何去選擇哪個執行緒去初始化?
實際上,在初始化陣列時使用了樂觀鎖CAS操作來決定到底是哪個執行緒有資格進行初始化。volatile變數(sizeCtl
):它是一個標記位,用來告訴其他執行緒這個坑位有沒有人在,其執行緒間的可見性由volatile保證,CAS操作保證了設定sizeCtl標記位的可見性,保證了只有一個執行緒能設定成功。
/**
* 初始化table, 使用sizeCtl作為初始化容量.
*/
private final Node<K, V>[] initTable() {
Node<K, V>[] tab;
int sc;
while ((tab = table) == null || tab.length == 0) { //自旋直到初始化成功
if ((sc = sizeCtl) < 0) // sizeCtl<0 說明table已經正在初始化/擴容,此時會讓出CPU時間片
Thread.yield();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 將sizeCtl更新成-1,表示正在初始化中,如果CAS操作成功了,代表本執行緒將負責初始化工作
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); // n - (n >>> 2) = n - n/4 = 0.75n, 前面說了loadFactor已在JDK1.8廢棄
}
} finally {
sizeCtl = sc; // 設定threshold = 0.75 * table.length
}
break;
}
}
return tab;
}
2.table[i]對應的桶為空: 直接CAS操作佔用桶table[i];
3.發現ForwardingNode結點,說明此時table正在擴容,則嘗試協助進行資料遷移。後面會對helpTransfer()呼叫的核心方法transfer()進行分析,我這裡簡單瞭解一下helpTransfer():
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { //sizeCtl加一,表示多一個執行緒進來協助擴容
transfer(tab, nextTab); //擴容,後面詳細講解
break;
}
}
return nextTab;
}
return table;
}
4.出現hash衝突,也就是table[i]桶中已經有了結點
- 當兩個不同key對映到同一個 table[i] 桶中時,就會出現這種情況:
- 當table[i]的結點型別為Node——連結串列結點時,就會將新結點以“尾插法”的形式插入連結串列的尾部;
- 當table[i]的結點型別為TreeBin——紅黑樹代理結點時,就會將新結點透過紅黑樹的插入方式插入。
get()方法
/**
* 根據key查詢對應的value值
*
* @return 查詢不到則返回null
* @throws NullPointerException if the specified key is null
*/
public V get(Object key) {
Node<K, V>[] tab;
Node<K, V> e, p;
int n, eh;
K ek;
int h = spread(key.hashCode()); // 重新計算key的hash值
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) { // table[i]就是待查詢的項,直接返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
} else if (eh < 0) // hash值<0, 說明遇到特殊結點(非連結串列結點), 呼叫find方法查詢
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { // 按連結串列方式查詢
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
查詢流程大致如下:
- 首先:根據key的hash值計算對映到table的哪個桶,table[i];
- 其次:如果table[i]的key和待查詢key相同,那直接返回(這時候不用判斷是不是特殊節點);
- 最後:如果table[i]對應的結點是特殊結點(hash值小於0),則透過 find() 查詢,如果不是特殊節點,則按連結串列查詢;
注意,假設現在需要get一個下標為3的結點,但此時桶table[3]的節點正在遷移,突然有一個執行緒進來呼叫get方法,正好key又雜湊到桶table[3],此時怎麼辦?
此時使用的查詢方法就不是get()
了,而是find()
。
find()方法
- Node結點的查詢(hash>=0)
當槽table[i]被普通Node結點佔用,說明是連結串列連結的形式,直接從連結串列頭開始查詢:
/**
* 連結串列查詢.
*/
Node<K, V> find(int h, Object k) {
Node<K, V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
- TreeBin結點的查詢 (hash=-2)
TreeBin的查詢比較特殊,我們知道當桶table[i]被TreeBin結點佔用時,說明連結的是一棵紅黑樹,並且由於紅黑樹的插入、刪除等操作會涉及整個結構的調整,所以通常存在讀寫併發操作的時候,是需要加鎖的。
/**
* 從根結點開始遍歷查詢,找到“相等”的結點就返回它,沒找到就返回null
* 當存在寫鎖或等待獲取寫鎖時,以連結串列方式進行查詢
* 也就是說,只有讀鎖時,才紅黑樹查詢
*/
final Node<K, V> find(int h, Object k) {
if (k != null) {
for (Node<K, V> e = first; e != null; ) {
int s;
K ek;
/**
* 兩種特殊情況下以連結串列的方式進行查詢:
* 1. WRITER---》有執行緒正持有寫鎖,這樣做能夠不阻塞讀執行緒
* 2. WAITER ---》有執行緒等待獲取寫鎖,不再繼續加讀鎖,相當於“寫優先”模式
*/
if (((s = lockState) & (WAITER | WRITER)) != 0) {
if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next; // 連結串列形式查詢,找到立即返回
}
//這時候就是按紅黑樹找了,讀執行緒數量加1(讀讀),讀狀態進行累加, READER==4
else if (U.compareAndSwapInt(this, LOCKSTATE, s, s + READER)) {
TreeNode<K, V> r, p;
try {
p = ((r = root) == null ? null : r.findTreeNode(h, k, null)); //紅黑樹的根節點非空才能找
} finally {
Thread w;
// 如果去除當前讀執行緒狀態,LOCKSTATE依舊錶示為有寫執行緒w因為讀鎖而阻塞並有讀執行緒,則告訴寫執行緒,它可以嘗試獲取寫鎖了,就是條件2
if (U.getAndAddInt(this, LOCKSTATE, -READER) == (READER | WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
- ForwardingNode結點的查詢 (hash=-1)
ForwardingNode是一種臨時結點,在擴容進行中才會出現,所以查詢也在擴容的table ----》nextTable 上進行 (連結串列遍歷)。
/**
* 在新的擴容table—-》nextTable上進行查詢
*/
Node<K, V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer:
for (Node<K, V>[] tab = nextTable; ; ) {
Node<K, V> e;
int n;
if (k == null || tab == null || (n = tab.length) == 0 || (e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (; ; ) {
int eh;
K ek;
if ((eh = e.hash) == h && ((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K, V>) e).nextTable;
continue outer;
} else
return e.find(h, k); //連結串列遍歷
}
if ((e = e.next) == null)
return null;
}
}
}
- ReservationNode結點的查詢
ReservationNode是保留結點,不儲存實際資料,所以直接返回null
Node<K, V> find(int h, Object k) {
return null;
}
計數方法
ConcurrentHashMap由於存在多執行緒的情況,所以其相關的計數方法也需要進行特殊處理。
ConcurrentHashMap中使用size()
方法計算鍵值對的數目:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}
sumCount()的原始碼:
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;
}
CounterCell
這個槽物件,當出現併發衝突時,每個執行緒會根據自己的hash值找到對應的槽位置。
/**
* 計數槽.
* 類似於LongAdder中的Cell內部類
*/
static final class CounterCell {
volatile long value;
CounterCell(long x) {
value = x;
}
}
之前在putVal()
方法中,新增新結點後會使用addCount()
進行技術加1,原始碼如下:
/**
* 更改計數值,並檢查長度是否達到閾值
*/
private final void addCount(long x, int check) {
CounterCell[] as; //計數桶
long b, s;
// !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x):嘗試更新baseCount
//1、如果counterCells不為null,則代表已經初始化了,直接進入if語句塊
//2、若競爭不嚴重,counterCells有可能還未初始化,為null,先嚐試CAS操作遞增baseCount值
if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//進入此語句塊有兩種可能:
//1.counterCells被初始化完成了,不為null
//2.CAS操作遞增baseCount值失敗了,說明出現併發衝突,則將計數值累加到Cell槽
CounterCell a;
long v;
int m;
boolean uncontended = true; //標誌是否存在競爭
//1.先判斷計數桶是否初始化,如果as=null,說明沒有,進入語句塊
//2.判斷計數桶長度是否為空,若是進入語句塊
//3.這裡做了一個執行緒變數隨機數,若桶的這個位置為空,進入語句塊(根據執行緒hash值計算槽索引)
//4.到這裡說明桶已經初始化了,且隨機的這個位置不為空,嘗試CAS操作使桶加1,失敗進入語句塊
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); // 槽更新也失敗, 則會執行fullAddCount
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;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
} else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount(); //統計容器大小
}
}
}
當出現了併發衝突,則不會再用CAS方式來計數了,直接使用桶方式,從上面的addCount方法可以看出來,此時的countCell是為空的(或者不為空但CAS更新失敗),最終一定會進入fullAddCount方法來進行初始化桶。
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) {
CounterCell[] as; CounterCell a; int n; long v;
...
//如果計數桶!=null,證明已經初始化,此時不走此語句塊
if ((as = counterCells) != null && (n = as.length) > 0) {
...
}
//進入此語句塊進行計數桶的初始化
//CAS設定cellsBusy=1,表示現在計數桶Busy中...
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
//若有執行緒同時初始化計數桶,由於CAS操作只有一個執行緒進入這裡
boolean init = false;
try { // Initialize table
//再次確認計數桶為空
if (counterCells == as) {
//初始化一個長度為2的計數桶
CounterCell[] rs = new CounterCell[2];
//h為一個隨機數,與上1則代表結果為0、1中隨機的一個
//也就是在0、1下標中隨便選一個計數桶,x=1,放入1的值代表增加1個容量
rs[h & 1] = new CounterCell(x);
//將初始化好的計數桶賦值給ConcurrentHashMap
counterCells = rs;
init = true;
}
} finally {
//最後將busy標識設定為0,表示不busy了
cellsBusy = 0;
}
if (init)
break;
}
//若有執行緒同時來初始化計數桶,則沒有搶到busy資格的執行緒就先來CAS遞增baseCount
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
從上面原始碼中可以看出,在CAS操作遞增計數桶失敗了3次之後,會進行擴容計數桶操作,注意此時同時進行了兩次隨機定位計數桶來進行CAS遞增的,所以此時可以保證大機率是因為計數桶不夠用了,才會進行計數桶擴容。
計數總結
- 1、利用CAS遞增
baseCount
值來感知是否存線上程競爭,若競爭不大直接CAS遞增baseCount
值即可,效能與直接baseCount++
差別不大; - 2、若存線上程競爭,則初始化計數桶,若此時初始化計數桶的過程中也存在競爭,多個執行緒同時初始化計數桶,則沒有搶到初始化資格的執行緒直接嘗試
CAS
遞增baseCount
值的方式完成計數,最大化利用了執行緒的並行。此時使用計數桶計數,分而治之的方式來計數,此時兩個計數桶最大可提供兩個執行緒同時計數,同時使用CAS操作來感知執行緒競爭,若兩個桶情況下CAS
操作還是頻繁失敗(失敗3
次),則直接擴容計數桶,變為4
個計數桶,支援最大同時4
個執行緒併發計數,以此類推…同時使用位運算和隨機數的方式"負載均衡"一樣的將執行緒計數請求接近均勻的落在各個計數桶中。
擴容機制
透過前面相關介紹,我們知道,當往table[i]中插入結點時,如果連結串列的結點數目超過一定閾值(8),就會觸發連結串列 -> 紅黑樹的轉換,這樣提高了查詢效率。
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i); // 連結串列 -> 紅黑樹 轉換
接下來我們分析這個 連結串列 -> 紅黑樹 的轉換操作,treeifyBin(tab, i)
。
/*
* 連結串列 -> 紅黑樹 轉換
*/
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n;
if (tab != null) {
// CASE 1: table的容量 < MIN_TREEIFY_CAPACITY時,直接進行table擴容,不進行紅黑樹轉換,MIN_TREEIFY_CAPACITY預設為64
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
// CASE 2: table的容量 ≥ MIN_TREEIFY_CAPACITY時,進行相應桶的連結串列 -> 紅黑樹的轉換
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;
}
// 以TreeBin型別包裝,並連結到table[index]中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
透過 treeifyBin(Node<K,V>[] tab, int index) 原始碼可以看出,連結串列 -> 紅黑樹這一轉換並不是一定會進行的:
- 當桶的容量 < MIN_TREEIFY_CAPACITY(64),CurrentHashMap 會首先選擇擴容(呼叫 tryPresize() 把陣列長度擴大到原來的兩倍),而非立即轉成紅黑樹;
- 當桶的容量 >= MIN_TREEIFY_CAPACITY(64),則選擇 連結串列 -> 紅黑樹。
再看一下 tryPresize() 如何執行擴容:
/*
* 嘗試對table陣列進行擴容
* @param 待擴容的大小
*/
private final void tryPresize(int size) { //jdk16
// 視情況將size調整為2的整數次冪,與0.5 * MAXIMUM_CAPACITY來比較 , tableSizeFor求二次冪
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
//CASE 1: table還未初始化,則先進行初始化
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c; //取最大值
if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
// CASE2: c <= sc說明已經被擴容過了;n >= MAXIMUM_CAPACITY說明table陣列已達到最大容量
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// CASE3: 進行table擴容
else if (tab == table) {
int rs = resizeStamp(n);
// 這個CAS操作可以保證,僅有一個執行緒會執行擴容
if (U.compareAndSetInt(this , SIZECTL , sc , (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
擴容原理
透過tryPresize() 我們發現呼叫了transfer方法,該方法可以被多個執行緒同時呼叫,是“資料遷移”的核心操作方法, 接下來我們看一看
/**
* 資料轉移和擴容.
* 每個呼叫tranfer的執行緒會對當前舊table中[transferIndex-stride, transferIndex-1]位置的結點進行遷移
*
* @param tab 舊table陣列
* @param nextTab 新table陣列
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride可理解成“步長”,即“資料遷移”時,每個執行緒要負責舊table中的多少個桶,根據幾核的CPU決定“步長”,最少16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) //MIN_TRANSFER_STRIDE預設16
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // 第二個引數,nextable為null說明第一次擴容
try {
@SuppressWarnings("unchecked")
// 建立新table陣列,擴大一倍,32,n還為16
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // 處理記憶體溢位(OOME)的情況
sizeCtl = Integer.MAX_VALUE; //將表示容量的sizeCtl 設定為最大值,然後返回
return;
}
nextTable = nextTab; //設定nextTable變數為擴容後的陣列
transferIndex = n; // [transferIndex-stride, transferIndex-1]:表示當前執行緒要進行資料遷移的桶區間
}
int nextn = nextTab.length;
// ForwardingNode結點,當舊table的某個桶中的所有結點都遷移完後,用該結點佔據這個桶
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 標識一個桶的遷移工作是否完成,advance == true 表示可以進行下一個位置的遷移
boolean advance = true;
// 最後一個資料遷移的執行緒將該值置為true,並進行本輪擴容的收尾工作
boolean finishing = false; // to ensure sweep before committing nextTab
// i標識桶索引, bound標識邊界
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 每一次自旋前的預處理,主要是為了定位本輪處理的桶區間
// 正常情況下,預處理完成後:i == transferIndex-1:右邊界;bound == transferIndex-stride:左邊界
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// CASE1:當前是處理最後一個tranfer任務的執行緒或出現擴容衝突
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 所有桶遷移均已完成
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 擴容執行緒數減1,表示當前執行緒已完成自己的transfer任務
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 判斷當前執行緒是否是本輪擴容中的最後一個執行緒,如果不是,則直接退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
/**
* 最後一個資料遷移執行緒要重新檢查一次舊table中的所有桶,看是否都被正確遷移到新table了:
* ①正常情況下,重新檢查時,舊table的所有桶都應該是ForwardingNode;
* ②特殊情況下,比如擴容衝突(多個執行緒申請到了同一個transfer任務),此時當前執行緒領取的任務會作廢,那麼最後檢查時,
* 還要處理因為作廢而沒有被遷移的桶,把它們正確遷移到新table中
*/
i = n; // recheck before commit
}
}
// CASE2:舊桶本身為null,不用遷移,直接嘗試放一個ForwardingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// CASE3:該舊桶已經遷移完成,直接跳過
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// CASE4:該舊桶未遷移完成,進行資料遷移
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// CASE4.1:桶的hash>0,說明是連結串列遷移
if (fh >= 0) {
/**
* 下面的過程會將舊桶中的連結串列分成兩部分:ln鏈和hn鏈
* ln鏈會插入到新table的槽i中,hn鏈會插入到新table的槽i+n中
*/
int runBit = fh & n; // 由於n是2的冪次,所以runBit要麼是0,要麼高位是1
Node<K,V> lastRun = f; // lastRun指向最後一個相鄰runBit不同的結點
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
// 以lastRun所指向的結點為分界,將連結串列拆成2個子連結串列ln、hn
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln); // ln連結串列存入新桶的索引i位置
setTabAt(nextTab, i + n, hn); // hn連結串列存入新桶的索引i+n位置
setTabAt(tab, i, fwd); // 設定ForwardingNode佔位
advance = true; // 表示當前舊桶的結點已遷移完畢
}
// CASE4.2:紅黑樹遷移
else if (f instanceof TreeBin) {
/**
* 下面的過程會先以連結串列方式遍歷,複製所有結點,然後根據高低位組裝成兩個連結串列;
* 然後看下是否需要進行紅黑樹轉換,最後放到新table對應的桶中
*/
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// 判斷是否需要進行 紅黑樹 <-> 連結串列 的轉換
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd); // 設定ForwardingNode佔位
advance = true; // 表示當前舊桶的結點已遷移完畢
}
else if (f instanceof ReservationNode) //jdk16特有,1.8沒有
throw new IllegalStateException("Recursive update");
}
}
}
}
}
至此,ConcurrentHashMap的大體原始碼就分析完畢啦!