ConcurrentHashMap
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
...
}
1. 一些重要引數
1.1 MAXIMUM_CAPACITY引數
/**
* The largest possible table capacity. This value must be
* exactly 1<<30 to stay within Java array allocation and indexing
* bounds for power of two table sizes, and is further required
* because the top two bits of 32bit hash fields are used for
* control purposes.
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
MAXIMUM_CAPACITY
參數列示map的最大容量,預設為1 << 30。
1.2 DEFAULT_CAPACITY引數
/**
* The default initial table capacity. Must be a power of 2
* (i.e., at least 1) and at most MAXIMUM_CAPACITY.
*/
private static final int DEFAULT_CAPACITY = 16;
DEFAULT_CAPACITY
參數列示map的預設容量,為16。
1.3 MAX_ARRAY_SIZE引數
/**
* The largest possible (non-power of two) array size.
* Needed by toArray and related methods.
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
MAX_ARRAY_SIZE
參數列示map的陣列最大長度,在toArray()
及其相關方法中可能用到。大小為Integer.MAX_VALUE - 8
。
1.4 DEFAULT_CONCURRENCY_LEVEL引數
/**
* The default concurrency level for this table. Unused but
* defined for compatibility with previous versions of this class.
*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
DEFAULT_CONCURRENCY_LEVEL
參數列示預設併發級別。在筆者當前使用版本JDK13中已經被棄用,但為了和先前版本相容,保留這個引數。
1.5 LOAD_FACTOR引數
/**
* The load factor for this table. Overrides of this value in
* constructors affect only the initial table capacity. The
* actual floating point value isn't normally used -- it is
* simpler to use expressions such as {@code n - (n >>> 2)} for
* the associated resizing threshold.
*/
private static final float LOAD_FACTOR = 0.75f;
LOAD_FACTOR
參數列示載入因子,預設和HashMap
一樣,是0.75。
1.6 TREEIFY_THRESHOLD引數
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2, and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
TREEIFY_THRESHOLD
參數列示陣列中連結串列轉換為紅黑樹的閾值,他被用於與一個連結串列的長度進行比較。
1.7 UNTREEIFY_THRESHOLD引數
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
UNTREEIFY_THRESHOLD
參數列示陣列中紅黑樹轉變成連結串列的閾值,他被用於與一個紅黑樹的大小進行比較。
1.8 MIN_TREEIFY_CAPACITY引數
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* The value should be at least 4 * TREEIFY_THRESHOLD to avoid
* conflicts between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
MIN_TREEIFY_CAPACITY
參數列示將連結串列樹化的雜湊表最小容量。只有當整個ConcurrentHashMap
的容量大於這個值時,具體的連結串列才可能發生樹化。如果沒有大於這個值,將進行擴容而非樹化。(擴容也會減少單個連結串列中的元素數量)。
1.9 MIN_TRANSFER_STRIDE引數
/**
* Minimum number of rebinnings per transfer step. Ranges are
* subdivided to allow multiple resizer threads. This value
* serves as a lower bound to avoid resizers encountering
* excessive memory contention. The value should be at least
* DEFAULT_CAPACITY.STR
*/
private static final int MIN_TRANSFER_STRIDE = 16;
在擴容操作中,transfer
這個步驟允許多執行緒併發進行,MIN_TRANSFER_STRIDE
參數列示進行一次transfer
操作中一個工作執行緒的最小任務量。即最少要處理的連續雜湊桶的數目,預設為16,即最少要對連續的16個雜湊桶進行transfer
操作,詳見下文transfer()
方法的解析。
1.10 RESIZE_STAMP_BITS引數(未理解)
/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static final int RESIZE_STAMP_BITS = 16;
RESIZE_STAMP_BITS
引數用於生成在每次擴容中都唯一的生成戳。
1.11 MAX_RESIZERS引數(未理解)
/**
* The maximum number of threads that can help resize.
* Must fit in 32 - RESIZE_STAMP_BITS bits.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
這個引數定義了resize
時工作執行緒的最大數量,但這個計算方法我不明白。MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
1.12 RESIZE_STAMP_SHIFT引數(未理解)
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
這個引數定義了sizeCtl
中記錄大小標記的位移位,但這個計算方法我不明白。MAX_RESIZERS = 32 - RESIZE_STAMP_BITS;
1.13 特殊節點的hash狀態引數
/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
正常情況下hash
的值都應該是正數,如果是負數說明當前是一個不正常的、特殊的節點。
hash
值為-1時,代表當前節點是一個Forwarding Node
。ForwardingNode
是一種臨時節點,在擴容進行中才會出現,並且它不儲存實際的資料。- 如果舊陣列的一個
hash
桶中全部的節點都遷移到新陣列中,舊陣列就在這個hash
桶中放置一個ForwardingNode
. - 讀操作或者迭代讀時碰到
ForwardingNode
時,將操作轉發到擴容後的新的table
陣列上去執行,寫操作碰見它時,則嘗試幫助擴容。
hash
值為-2時,代表當前節點是一個TreeBin
。TreeBin
是ConcurrentHashMap
中用於代理操作TreeNode
的特殊節點,持有儲存實際資料的紅黑樹的根節點。- 因為紅黑樹進行寫入操作,整個樹的結構可能會有很大的變化,這個對讀執行緒有很大的影響,所以
TreeBin
還要維護一個簡單讀寫鎖,這是相對HashMap
,這個類新引入這種特殊節點的重要原因。
hash
值為-3時,代表當前節點是一個保留節點,即佔位符。- 一般情況下不會出現。
1.14 HASH_BITS引數
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
HASH_BITS
在HashTable
中也見到過,通過和它的位運算,可以將負數的雜湊值轉變為正數。
1.15 NCPU引數
/** Number of CPUS, to place bounds on some sizings */
static final int NCPU = Runtime.getRuntime().availableProcessors();
NCPU
引數可以獲取當前JVM能使用的處理器核心數。
2. 一些重要屬性
值得關注的是,ConcurrentHashMap
中的關鍵屬性基本都是volatile
變數。
2.1 table屬性
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*/
transient volatile Node<K,V>[] table;
table
屬性用於存節點,是桶的集合。
2.2 nextTable屬性
/**
* The next table to use; non-null only while resizing.
*/
private transient volatile Node<K,V>[] nextTable;
nextTable
屬性表示下一個要使用的陣列,輔助resize
操作,也僅在resize
時非空。
2.3 baseCount屬性
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
private transient volatile long baseCount;
baseCount
屬性是在沒有爭用現象時的基本計數器值,也在初始化表的競爭中使用。
2.4 sizeCtl屬性
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
sizeCtl
屬性在表初始化和resize
操作控制中發揮作用。
- 當
sizeCtl
為負數,說明表正在進行初始化或resize
操作。- 表初始化時為 -1。
- 表
resize
時為-(1+擴容執行緒數)
。
- 當
sizeCtl
為正數。- 表為
null
時為初始表大小或 0。 - 表不為
null
時為需進行resize
的下一個計數值。
- 表為
2.5 transferIndex屬性
/**
* The next table index (plus one) to split while resizing.
*/
private transient volatile int transferIndex;
resize
中要拆分的下一個表索引。
2.6 cellsBusy屬性
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
*/
private transient volatile int cellsBusy;
在resize
過程和/或建立CounterCells
過程中使用的自旋鎖。
2.7 counterCells陣列
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells;
顯然,這是CounterCell
的陣列,即計數單元的陣列。
3. 內部類
3.1 Node內部類
Node
內部類是ConcurrentHashMap
類中普通節點的抽象。
/**
* Key-value entry. This class is never exported out as a
* user-mutable Map.Entry (i.e., one supporting setValue; see
* MapEntry below), but can be used for read-only traversals used
* in bulk tasks. Subclasses of Node with a negative hash field
* are special, and contain null keys and values (but are never
* exported). Otherwise, keys and vals are never null.
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val) {
this.hash = hash;
this.key = key;
this.val = val;
}
Node(int hash, K key, V val, Node<K,V> next) {
this(hash, key, 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 Helpers.mapEntryToString(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)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
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;
}
}
意義
Node
內部類作為ConcurrentHashMap
節點的實現。
hashCode()的實現
注意hashCode()
的實現方式:Objects.hashCode(key) ^ Objects.hashCode(value);
find()
這裡Node
內部類的find()
方法其實不會在一般的業務方法如get()
中被呼叫,因為在那些地方會直接遍歷。這個方法會在ForwardingNode
類的find()
方法中被呼叫。
4. 工具方法
4.1 spread方法
/**
* Spreads (XORs) higher bits of hash to lower and also forces top
* bit to 0. Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
通過取高位再進行掩碼計算(保證雜湊值為正),來減少雜湊衝突。
這個方法就是所謂的擾動方法。
4.2 tableSizeFor方法
/**
* Returns a power of two table size for the given desired capacity.
* See Hackers Delight, sec 3.2
*/
private static final int tableSizeFor(int c) {
int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
tableSizeFor
方法用於計算引數c
對應的resize
閾值。往往出現為下面的語句。
4.3 comparableClassFor方法
/**
* Returns x's Class if it is of the form "class C implements
* Comparable<C>", else null.
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; ParameterizedType p;
// 如果是String 直接返回
if ((c = x.getClass()) == String.class)
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (Type t : ts) {
if ((t instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
如果引數x
是一個Comparable
介面的實現類,那麼返回它的型別。
4.4 compareComparables方法
/**
* Returns k.compareTo(x) if x matches kc (k's screened comparable
* class), else 0.
*/
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
如果物件x
匹配k
的可比類kc
,那麼返回k.compareTo(x)
,否則返回0。
4.5 列表元素訪問方法
4.5.1 tabAt方法
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}
tabAt()
方法可以獲得在 i 位置上的 Node 節點。
4.5.2 casTabAt方法
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSetReference(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
casTabAt()
方法可以以CAS的方式更新 i 位置上的 Node 節點
4.5.3 setTabAt方法
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}
setTabAt
方法可以設定在 i 位置上的 Node 節點。
注:像Unsafe.getReferenceAcquire()
方法和Unsafe.putReferenceRelease()
方法這樣的方法實際上是Unsafe
中volatile
方法的發行版本。如後者是putReferenceVolatile()
的發行版本。
4.6 initTable方法
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,說明正在初始化或resize,自旋
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {// 如果SIZECTL仍是sc,則置為-1.表示進入初始化
try {
if ((tab = table) == null || tab.length == 0) {
// 獲取初始大小(sc為正時即為初始大小)
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 建立一個node陣列
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 為table屬性賦值
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
// 最後記得更新sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
initTable()
方法可以初始化一個空表。
4.7 hashCode方法
public int hashCode() {
int h = 0;
Node<K,V>[] t;
if ((t = table) != null) {
Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
for (Node<K,V> p; (p = it.advance()) != null; )
h += p.key.hashCode() ^ p.val.hashCode();
}
return h;
}
hashCode()
方法就是遍歷每個鍵值對,令他們的鍵和值的雜湊碼相異或,再全部疊加起來。
4.8 addCount方法
addCount()
方法會在ConcurrentHashMap
元素數量變化時被呼叫,兩個引數中第一個為數量變化值,第二個為控制是否需要擴容檢查的引數。
private final void addCount(long x, int check) {
// 建立計數單元CounterCell
CounterCell[] cs; long b, s;
/**
1.如果counterCells為null:
那麼說明之前沒有發生過併發衝突,隨後會執行U.compareAndSetLong(...,b+x),直接更新了計數值baseCount。而這個本地方法執行成功會返回true,取反後為false。那麼整個這個if判斷兩個條件都為false,不執行if塊內內容。
2.如果couterCells不為null:
那麼說明之前發生過併發衝突,需要下面的if塊處理。這裡if的第一個條件為ture,第二個條件的更新方法就不會執行。
*/
if ((cs = counterCells) != null
|| !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
// 進入if塊,說明曾發生過併發衝突,那麼要把值加到CounterCell中
CounterCell c; long v; int m;
boolean uncontended = true;
if (cs == null // cs在併發中又變成了null
|| (m = cs.length - 1) < 0 // cs長度小於1
|| (c = cs[ThreadLocalRandom.getProbe() & m]) == null // 對應的CouterCell為null
|| !(uncontended = U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {// 嘗試更新找到的計數單元c的值
// 如果更新失敗。一般就是上面的最後一個條件中的方法返回了false,取反後為true
// 說明CounterCells陣列中出現了併發衝突,可能涉及該陣列的擴容,呼叫fullAddCount方法
fullAddCount(x, uncontended);
return;
}
if (check <= 1)// 如果無需檢查,直接返回
return;
// 計數,存到s中,下面用於做檢查
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) << RESIZE_STAMP_SHIFT;
// 如果正在執行resize
if (sc < 0) {
// 一些放棄幫助擴容的條件
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
// 將sc+1,表示一個新執行緒加入幫助擴容
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 當前沒在執行resize,嘗試成為第一個進入擴容的執行緒。將sc設定為rs+2
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
// 重新計算元素數量
s = sumCount();
}
}
}
詳細的邏輯看程式碼註釋,這裡單獨講幾個點。
- 第一個if的判斷條件的使用很精彩,根據
CounterCells
陣列是否為null
來檢查是該直接把值加到baseCount
上還是加到對應的CounterCell
中。 - 注意在
CounterCells
陣列中找到槽位置的方式:c = cs[ThreadLocalRandom.getProbe() & m]) == null
。 check
引數小於等於1時,不用做檢查就退出。大於1時,要在addCount
的主邏輯結束之後在檢查需不需要做擴容。在put
方法呼叫addCount
時,傳入的check
引數其實是put
過程中遍歷到的節點數量,這樣邏輯就連通了:假如原先就只有一個節點或為空,就不需要考慮是否需要再檢查擴容;否則就要在addCoumt
中檢查。
4.9 helpTransfer方法
helpTransfer
方法可以在節點正在resize
時協助資料遷移並返回新陣列。在put
、remove
等業務方法中都有呼叫這個方法。
/**
* Helps transfer if a resize is in progress.
*/
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)// f 是一個Forwarding Node
&& (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) // nextTable不空
{
// 計算出本次resize時的標記“戳”
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
while (nextTab == nextTable // nextTab不變
&& table == tab // table不變
&& (sc = sizeCtl) < 0) // sizeCtl保持小於0(正在resize)
{
if (sc == rs + MAX_RESIZERS // 工作執行緒數量已滿
|| sc == rs + 1 // 在addCount方法中,假如有第一個擴容執行緒,sc=rs+2。假如變成rs+1,說明擴容結束。
|| transferIndex <= 0) // 如果transferIndex小於等於0,實際說明擴容工作已完成,已進入下標調整。
break;
// 令sc++,進入擴容
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
// 返回新表
return nextTab;
}
// 返回原表
return table;
}
4.10 transfer方法
transfer
將方法的功能是將每個 bin 中的節點移動和/或複製到新表。 在addCount()
和helpTransfer()
中都有呼叫,是擴容工作的核心實現類。
下面例子中若出現具體數字,以傳入tab的length為16為準。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// 定義n為表長。
int n = tab.length, stride;
/**
stride表示一次transfer中一個工作執行緒的任務量,即要處理的連續雜湊桶的數目。
初始化stride:假如可用CPU核數大於1,初始化為(n >>> 3) / NCPU,否則初始化為n。
假如初始化後的stride小於MIN_TRANSFER_STRIDE,把他置為這個最小值。
*/
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // nextTab未初始化,就要先初始化這個陣列
try {
@SuppressWarnings("unchecked")‘
// 建立nextTab陣列,長度為原陣列長度*2
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
// 建立新陣列失敗,sizeCtl就設為int的最大值
sizeCtl = Integer.MAX_VALUE;
return;
}
// 這個陣列賦值給nextTable
nextTable = nextTab;
// 更新轉移下標
transferIndex = n;
}
int nextn = nextTab.length;
// 建立ForwardingNode fwd,傳入nextTab作為引數
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 首次推進為 true,如果等於 true,說明需要再次推進一個下標(i--),反之,如果是 false,那麼就不能推進下標,需要將當前的下標處理完畢才能繼續推進
boolean advance = true;
// 標記是否已經擴容完成
boolean finishing = false; // to ensure sweep before committing nextTab
/**
又是一個for迴圈自旋,處理每個槽位中的連結串列元素
*/
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
/**
這個while迴圈通過CAS不斷嘗試為當前執行緒分配任務,直到分配成功或任務佇列已經被全部分配完畢。
如果執行緒已經被分配過bucket區域,那麼會通過--i指向下一個待處理桶然後退出迴圈。
*/
while (advance) {
int nextIndex, nextBound;
// --i表示進入下一個待處理的bucket。自減後大於等於bound,表明當前執行緒已經分配過bucket,advance=false
if (--i >= bound || finishing)
advance = false;
// 所有bucket已經被分配完畢,為nextIndex賦值。
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// CAS修改TRANSFERINDEX,為執行緒分配任務。
// 處理的節點區間為(nextBound,nextINdex)
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
// 處理過程
// CASE1:舊陣列已遍歷完成,當前執行緒已經處理完所有負責的bucket
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 擴容已完成
if (finishing) {
// 刪除這個成員變數nextTable
nextTable = null;
// 更新陣列
table = nextTab;
// 更新擴容閾值
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 使用 CAS 操作對 sizeCtl 的低16位進行減 1,代表做完了屬於自己的任務
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果上面的if沒有執行,即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
// 說明當前已經沒有擴容執行緒了,擴容結束了
finishing = advance = true;
i = n; // recheck before commit
}
}
// CASE2:節點i處為空,那麼放入剛剛初始化的ForwardingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// CASE3:該位置現在的雜湊值就是MOVED,是一個ForwardingNode,說明已經被其他執行緒處理過,那麼要求繼續推進
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
// CASE4:執行轉移
else {
// 鎖住頭節點
synchronized (f) {
// 再次檢查
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 槽中頭節點是一個連結串列頭節點
if (fh >= 0) {
// 先計算好當前的fh * n
int runBit = fh & n;
// 儲存遍歷最終位置的lastRun
Node<K,V> lastRun = f;
// 遍歷連結串列
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
// 如果遍歷過程中出現hash&n出現了變化,需要更新runBit和lastRun
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//如果lastRun引用的是低位連結串列,令ln為lastRun
if (runBit == 0) {
ln = lastRun;
hn = null;
}
// 如果lastRUn引用的是高位連結串列,令hn為lastRun
else {
hn = lastRun;
ln = null;
}
// 遍歷連結串列,把hash&n為0的放在低位連結串列中,不為0的放在高位連結串列中
// 迴圈跳出條件:當前迴圈結點!=lastRun
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);
// 高位連結串列的位置是:原位置 + n
setTabAt(nextTab, i + n, hn);
// 標記當前桶已遷移
setTabAt(tab, i, fwd);
// advance為true,返回上面進行--i操作
advance = true;
}
// 槽中頭節點是一個樹節點
else if (f instanceof TreeBin) {
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);
advance = true;
}
// 槽中頭節點是一個保留佔位節點
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
}
}
}
transfer()
方法是ConcurrentHashMap
執行擴容操作的核心方法,他的擴容轉移操作其實類似於HashMap
,都是將原連結串列分裂為兩個連結串列。
但也有很多實現細節上的不同,詳見原始碼註釋。
4.11 resizeStamp方法
/**
* Returns the stamp bits for resizing a table of size n.
* Must be negative when shifted left by RESIZE_STAMP_SHIFT.
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
resizeStamp(int n)
方法可以在擴容一個大小為n的table時,計算stamp bits
5. 業務方法
5.1 構造方法
// 預設構造方法
public ConcurrentHashMap() {
}
// 僅提供初始容量的構造方法
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, LOAD_FACTOR, 1);
}
// 提供map的構造方法
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
// 提供預設容量和載入因子的構造方法
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
// 提供預設容量、載入因子和併發更新執行緒數的構造方法。
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);
// cap賦值為最大容量或擴容閾值
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
5.2 size方法
// 計數單元陣列
private transient volatile CounterCell[] counterCells;
public int size() {
// 呼叫sumCount()
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
// 獲得計數單元陣列
CounterCell[] cs = counterCells;
long sum = baseCount;
if (cs != null) {
// 所有計數單元中的值加起來
for (CounterCell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
// 非常簡單的一個計數單元,僅有一個volatile型的計數器value
@jdk.internal.vm.annotation.Contended // 這個註解可以保證當前類的物件獨享快取行
static final class CounterCell {
// 只提供了構造器,沒有提供get/set方法,也就是value的值在初始化時確定,後續不再更改
volatile long value;
CounterCell(long x) { value = x; }
}
size()
方法的實現是首先獲取baseCount
,這是無爭用時獲取的計數器值。再將計數單元陣列中的計數值累加在上面。他有以下幾個保證執行緒安全的舉措:
- 將
counterCells
陣列和CounterCell
類中的value
變數設為volatile
。 - 沒有為
CounterCell
類中的value
變數設定get/set
方法。
那麼CounterCells
陣列是怎麼被建立和初始化的呢,baseCount
是怎麼增加的呢。後續結合到那些改變了size
的業務方法,如put()
等方法的原始碼,我們再來做解釋。
5.3 isEmpty方法
public boolean isEmpty() {
return sumCount() <= 0L; // ignore transient negative values
}
sumCount()
方法見5.2
5.4 get方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 計算雜湊值
int h = spread(key.hashCode());
if ((tab = table) != null // 表不為空
&& (n = tab.length) > 0 // 表長度不為0
&& (e = tabAt(tab, (n - 1) & h)) != null) {// 指定位置不為null
// 首位置即為要找的key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)// 當前連結串列頭雜湊值小於0,說明是一個特殊節點
// 呼叫特殊節點e的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
在雜湊表中的雜湊位置。然後根據找到的節點的雜湊值的不同,來做不同的處理。
- 如果雜湊值時所要找到的值,直接返回。
- 如果雜湊值小於0,代表當前節點是一個特殊節點。可參考
1.13 特殊節點的hash狀態引數
。這樣的話就會去呼叫特殊節點的find()
方法,如ForwardingNode
類和TreeNode
類的find()
方法。 - 如果雜湊值大於等於0,遍歷當前連結串列查詢。
5.5 containsKey方法
public boolean containsKey(Object key) {
return get(key) != null;
}
5.6 containsValue方法
public boolean containsValue(Object value) {
if (value == null)
throw new NullPointerException();
Node<K,V>[] t;
if ((t = table) != null) {
Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
for (Node<K,V> p; (p = it.advance()) != null; ) {
V v;
if ((v = p.val) == value || (v != null && value.equals(v)))
return true;
}
}
return false;
}
Traverser
類封裝了containsValue
方法的遍歷邏輯,程式碼較為複雜,這裡暫且按下不表。
5.7 put方法
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();
// 計算雜湊值
int hash = spread(key.hashCode());
// 當前桶的計數器
int binCount = 0;
// 自旋插入節點,直到成功
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
// CASE1:表為空則先呼叫初始化方法
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// CASE2:如果雜湊位置節點為空,向空位置插入時是無鎖的
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 嘗試把要put的鍵值對直接put到這裡
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;// 退出
}
// CASE3:如果雜湊位置節點雜湊值為-1,即為一個Forwarding Node,呼叫helperTransfer()
else if ((fh = f.hash) == MOVED)
// 協助轉移資料並獲取新陣列
tab = helpTransfer(tab, f);
// CASE4:如果onlyIfAbsent為true,且頭節點即為所需節點,直接返回它
else if (onlyIfAbsent
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
// CASE5:找到了指定位置,並且不為空(出現了雜湊衝突)。
else {
V oldVal = null;
synchronized (f) {// 鎖住當前節點(連結串列頭)
if (tabAt(tab, i) == f) {// 再判斷一下f是不是頭節點,防止它被別的執行緒已經修改了
// if-不是一個特殊節點
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;
}
// 如果到了尾部,就插入由當前key-value構建的新節點
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
// elseIf-是一個樹節點
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;
}
}
// else-如果是一個保留節點
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 插入完成後,檢查一下需不需要把當前連結串列樹化
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 計數器加一
addCount(1L, binCount);
// 返回null
return null;
}
具體邏輯見註釋,解釋得很詳細。
putVal
方法會用一個for迴圈保持自旋,不斷嘗試插入要求插入的鍵值對。迴圈內有以下幾個CASE,體現為if-else塊中的五個分支。
- 表為空。呼叫初始化方法。
- 雜湊位置為空。無鎖地直接put。
- 雜湊位置是一個
ForwardingNode
,呼叫helpTransfer
。 - 雜湊位置頭即為當前鍵,且
onlyIfAbsent
為true
,直接返回。 - 雜湊位置不為空,說明出現了雜湊衝突。
注意在遍歷過程中對binCount
的更新,最終用addCount()
令物件加一,並用binCount
作為check
引數。
5.8 remove方法
public V remove(Object key) {
return replaceNode(key, null, null);
}
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
// 自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// CASE1:可以直接退出的情況:陣列為空或雜湊結果位置為null。
if (tab == null
|| (n = tab.length) == 0
|| (f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
// CASE2:節點正在移動,協助移動
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// CASE3:出現雜湊衝突,要在連結串列中查詢
else {
V oldVal = null;
boolean validated = false;
// 鎖住頭節點
synchronized (f) {// 內部具體邏輯不再贅述,類似於上面的put方法
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
// e 表示當前迴圈處理結點,pred 表示當前迴圈節點的上一個節點
for (Node<K,V> e = f, pred = null;;) {
K ek;
// 找到
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (validated) {
if (oldVal != null) {
// 如果是一次刪除,元素個數減一
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
關鍵還是在於鎖住連結串列頭來實現執行緒安全。邏輯直接看原始碼即可。