一、概述
ConcurrentHashMap類實際上就是爲了解決HashMap的線程不安全而設計的類,ConcurrentHashMap類處於jdk的併發包下,在併發編程中有着非常重要的作用。
二、源碼分析
1. 類的聲明
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable
ConcurrentHashMap類繼承自AbstractMap類,實現了ConcurrentMap接口和Serializable接口。
2. 重要成員常量
//元素最大容量,也就是size能達到的最大值
private static final int MAXIMUM_CAPACITY = 1 << 30;
//容量初始化時小於等於16的初始容量
private static final int DEFAULT_CAPACITY = 16;
//數組最大長度
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//默認瑣的等級
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//默認負載因子
private static final float LOAD_FACTOR = 0.75f;
//鏈表轉樹的臨界元素數量,也就是鏈表存儲元素達到8個時就需要轉爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//樹轉鏈表的臨界元素數量,也就是紅黑樹的元素個數爲6個時就需要轉爲鏈表了
static final int UNTREEIFY_THRESHOLD = 6;
//當需要構建紅黑樹的時候,判斷底層桶數組長度必須不能小於64,如果數組長度小於64,那麼就進行桶擴容來rehash(),而不是創建紅黑樹。
static final int MIN_TREEIFY_CAPACITY = 64;
//擴容時,線程最低要求獲取的遷移任務,也就是每次最少遷移16個桶位,後續CAS設置transferIndex源碼中會看到
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;
//移位量,把生成戳移位後保存在sizeCtl中當做擴容線程計數的基數,相反方向移位後能夠反解出生成戳
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 下面幾個是特殊的節點的hash值,正常節點的hash值在hash函數中都處理過了,不會出現負數的情況,特殊節點在各自的實現類中有特殊的遍歷方法
// ForwardingNode的hash值,ForwardingNode是一種臨時節點,在擴進行中才會出現,並且它不存儲實際的數據
// 如果舊數組的一個hash桶中全部的節點都遷移到新數組中,舊數組就在這個hash桶中放置一個ForwardingNode
// 讀操作或者迭代讀時碰到ForwardingNode時,將操作轉發到擴容後的新的table數組上去執行,寫操作碰見它時,則嘗試幫助擴容
static final int MOVED = -1;
// TreeBin的hash值,TreeBin是ConcurrentHashMap中用於代理操作TreeNode的特殊節點,持有存儲實際數據的紅黑樹的根節點
// 因爲紅黑樹進行寫入操作,整個樹的結構可能會有很大的變化,這個對讀線程有很大的影響,
// 所以TreeBin還要維護一個簡單讀寫鎖,這是相對HashMap,這個類新引入這種特殊節點的重要原因
static final int TREEBIN = -2;
// ReservationNode的hash值,ReservationNode是一個保留節點,就是個佔位符,不會保存實際的數據,正常情況是不會出現的,
// 在jdk1.8新的函數式有關的兩個方法computeIfAbsent和compute中才會出現
static final int RESERVED = -3;
// 用於和負數hash值進行 & 運算,將其轉化爲正數(絕對值不相等),Hashtable中定位hash桶也有使用這種方式來進行負數轉正數
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
// CPU的核心數,用於在擴容時計算一個線程一次要幹多少活
static final int NCPU = Runtime.getRuntime().availableProcessors();
// 在序列化時使用,這是爲了兼容以前的版本
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("segments", Segment[].class),
new ObjectStreamField("segmentMask", Integer.TYPE),
new ObjectStreamField("segmentShift", Integer.TYPE)
};
ConcurrentHashMap類的成員常量非常多,但是很多我們已經在HashMap類中介紹過了,這裏值得注意的就是併發多線程情況下定義的一些規則以及標記狀態。
3. 重要成員變量
//底層的桶數組
transient volatile Node<K,V>[] table;
//key的集合視圖
private transient KeySetView<K,V> keySet;
//value的視圖
private transient ValuesView<K,V> values;
//Entry的視圖
private transient EntrySetView<K,V> entrySet;
// 擴容後的新的table數組,只有在擴容時纔有用
// nextTable != null,說明擴容方法還沒有真正退出,一般可以認爲是此時還有線程正在進行擴容,
// 極端情況需要考慮此時擴容操作只差最後給幾個變量賦值(包括nextTable = null)的這個大的步驟,
// 這個大步驟執行時,通過sizeCtl經過一些計算得出來的擴容線程的數量是0
private transient volatile Node<K,V>[] nextTable;
// 非常重要的一個屬性,源碼中的英文翻譯,直譯過來是下面的四行文字的意思
// sizeCtl = -1,表示有線程正在進行真正的初始化操作
// sizeCtl = -(1 + nThreads),表示有nThreads個線程正在進行擴容操作
// sizeCtl > 0,表示接下來的真正的初始化操作中使用的容量,或者初始化/擴容完成後的threshold
// sizeCtl = 0,默認值,此時在真正的初始化操作中使用默認容量
private transient volatile int sizeCtl;
// 下一個transfer任務的起始下標index 加上1 之後的值,transfer時下標index從length - 1開始往0走
// transfer時方向是倒過來的,迭代時是下標從小往大,二者方向相反,儘量減少擴容時transefer和迭代兩者同時處理一個hash桶的情況,
// 順序相反時,二者相遇過後,迭代沒處理的都是已經transfer的hash桶,transfer沒處理的,都是已經迭代的hash桶,衝突會變少
// 下標在[nextIndex - 實際的stride (下界要 >= 0), nextIndex - 1]內的hash桶,就是每個transfer的任務區間
// 每次接受一個transfer任務,都要CAS執行 transferIndex = transferIndex - 實際的stride,
// 保證一個transfer任務不會被幾個線程同時獲取(相當於任務隊列的size減1)
// 當沒有線程正在執行transfer任務時,一定有transferIndex <= 0,這是判斷是否需要幫助擴容的重要條件(相當於任務隊列爲空)
private transient volatile int transferIndex;
// 下面三個主要與統計數目有關,可以參考jdk1.8新引入的java.util.concurrent.atomic.LongAdder的源碼,幫助理解
// 計數器基本值,主要在沒有碰到多線程競爭時使用,需要通過CAS進行更新
private transient volatile long baseCount;
// CAS自旋鎖標誌位,用於初始化,或者counterCells擴容時
private transient volatile int cellsBusy;
// 用於高併發的計數單元,如果初始化了這些計數單元,那麼跟table數組一樣,長度必須是2^n的形式
private transient volatile CounterCell[] counterCells;
4. 普通鏈表結點-內部類Node
// 此類不會在ConcurrentHashMap以外被修改,只讀迭代可以利用這個類,迭代時的寫操作需要由另一個內部類MapEntry代理執行寫操作
// 此類的子類具有負數hash值,並且不存儲實際的數據,如果不使用子類直接使用這個類,那麼key和val永遠不會爲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, 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; }
// 不支持來自ConcurrentHashMap外部的修改,跟1.7的一樣,迭代操作需要通過另外一個內部類MapEntry來代理,迭代寫會重新執行一次put操作
// 迭代中可以改變value,是一種寫操作,此時需要保證這個節點還在map中,
// 因此就重新put一次:節點不存在了,可以重新讓它存在;節點還存在,相當於replace一次
// 設計成這樣主要是因爲ConcurrentHashMap並非爲了迭代操作而設計,它的迭代操作和其他寫操作不好併發,
// 迭代時的讀寫都是弱一致性的,碰見併發修改時儘量維護迭代的一致性
// 返回值V也可能是個過時的值,保證V是最新的值會比較困難,而且得不償失
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)));
}
// 從此節點開始查找k對應的節點
// 這裏的實現是專爲鏈表實現的,一般作用於頭結點,各種特殊的子類有自己獨特的實現
// 不過主體代碼中進行鏈表查找時,因爲要特殊判斷下第一個節點,所以很少直接用下面這個方法,
// 而是直接寫循環遍歷鏈表,子類的查找則是用子類中重寫的find方法
/** 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;
}
}
此類就是一個很普通的Entry節點,在鏈表形式保存才使用這種節點,它存儲實際的數據,基本結構類似於1.8的HashMap.Node,和1.7的Concurrent.HashEntry。
5. 普通樹結點-內部類TreeNode
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
// 新添加的prev指針是爲了刪除方便,刪除鏈表的非頭節點的節點,都需要知道它的前一個節點才能進行刪除,所以直接提供一個prev指針
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
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 爲根節點開始遍歷查找,跟HashMap.TreeNode.find實現一樣
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;
}
}
它也存儲有實際的數據,結構和1.8的HashMap的TreeNode一樣,一些方法的實現代碼也基本一樣。不過,ConcurrentHashMap對此節點的操作,都會由TreeBin來代理執行。也可以把這裏的TreeNode看出是有一半功能的HashMap.TreeNode,另一半功能在ConcurrentHashMap.TreeBin中。紅黑樹節點本身保存有普通鏈表節點Node的所有屬性,因此可以使用兩種方式進行讀操作。
6. 遷移時臨時結點-內部類ForwardingNode
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;
}
// ForwardingNode的查找操作,直接在新數組nextTable上去進行查找
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes 使用循環,避免多次碰到ForwardingNode導致遞歸過深
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) { // 繼續碰見ForwardingNode的情況,這裏相當於是遞歸調用一次本方法
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k); // 碰見特殊節點,調用其find方法進行查找
}
if ((e = e.next) == null) // 普通節點直接循環遍歷鏈表
return null;
}
}
}
}
ForwardingNode是一種臨時節點,在擴容進行中才會出現,hash值固定爲-1,並且它不存儲實際的數據數據。如果舊數組的一個hash桶中全部的節點都遷移到新數組中,舊數組就在這個hash桶中放置一個ForwardingNode。讀操作或者迭代讀時碰到ForwardingNode時,將操作轉發到擴容後的新的table數組上去執行,寫操作碰見它時,則嘗試幫助擴容。
7. 代理操作TreeNode的節點-內部類TreeBin
// 紅黑樹節點TreeNode實際上還保存有鏈表的指針,因此也可以用鏈表的方式進行遍歷讀取操作
// 自身維護一個簡單的讀寫鎖,不用考慮寫-寫競爭的情況
// 不是全部的寫操作都要加寫鎖,只有部分的put/remove需要加寫鎖
// 很多方法的實現和jdk1.8的ConcurrentHashMap.TreeNode裏面的方法基本一樣,可以互相參考
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; // 整體的鎖狀態標識位
// values for lockState
// 二進制001,紅黑樹的 寫鎖狀態
static final int WRITER = 1; // set while holding write lock
// 二進制010,紅黑樹的 等待獲取寫鎖的狀態,中文名字太長,後面用 WAITER 代替
static final int WAITER = 2; // set when waiting for write lock
// 二進制100,紅黑樹的 讀鎖狀態,讀鎖可以疊加,也就是紅黑樹方式可以併發讀,每有一個這樣的讀線程,lockState都加上一個READER的值
static final int READER = 4; // increment value for setting read lock
// 重要的一點,紅黑樹的 讀鎖狀態 和 寫鎖狀態 是互斥的,但是從ConcurrentHashMap角度來說,讀寫操作實際上可以是不互斥的
// 紅黑樹的 讀、寫鎖狀態 是互斥的,指的是以紅黑樹方式進行的讀操作和寫操作(只有部分的put/remove需要加寫鎖)是互斥的
// 但是當有線程持有紅黑樹的 寫鎖 時,讀線程不會以紅黑樹方式進行讀取操作,而是使用簡單的鏈表方式進行讀取,此時讀操作和寫操作可以併發執行
// 當有線程持有紅黑樹的 讀鎖 時,寫線程可能會阻塞,不過因爲紅黑樹的查找很快,寫線程阻塞的時間很短
// 另外一點,ConcurrentHashMap的put/remove/replace方法本身就會鎖住TreeBin節點,這裏不會出現寫-寫競爭的情況,因此這裏的讀寫鎖可以實現得很簡單
// 在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) {
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K,V> r = null;
for (TreeNode<K,V> x = b, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = r;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x);
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
/**
* Acquires write lock for tree restructuring.
*/
// 對根節點加 寫鎖,紅黑樹重構時需要加上 寫鎖
private final void lockRoot() {
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER)) // 先嚐試獲取一次 寫鎖
contendedLock(); // offload to separate method 單獨抽象出一個方法,直到獲取到 寫鎖 這個調用纔會返回
}
// 釋放 寫鎖
private final void unlockRoot() {
lockState = 0;
}
// 可能會阻塞寫線程,當寫線程獲取到寫鎖時,纔會返回
// ConcurrentHashMap的put/remove/replace方法本身就會鎖住TreeBin節點,這裏不會出現寫-寫競爭的情況
// 本身這個方法就是給寫線程用的,因此只用考慮 讀鎖 阻礙線程獲取 寫鎖,不用考慮 寫鎖 阻礙線程獲取 寫鎖,
// 這個讀寫鎖本身實現得很簡單,處理不了寫-寫競爭的情況
// waiter要麼是null,要麼是當前線程本身
private final void contendedLock() {
boolean waiting = false;
for (int s;;) {
// ~WAITER是對WAITER進行二進制取反,當此時沒有線程持有 讀鎖(不會有線程持有 寫鎖)時,這個if爲真
if (((s = lockState) & ~WAITER) == 0) {
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
// 在 讀鎖、寫鎖 都沒有被別的線程持有時,嘗試爲自己這個寫線程獲取 寫鎖,同時清空 WAITER 狀態的標識位
if (waiting) // 獲取到寫鎖時,如果自己曾經註冊過 WAITER 狀態,將其清除
waiter = null;
return;
}
}
else if ((s & WAITER) == 0) { // 有線程持有 讀鎖(不會有線程持有 寫鎖),並且當前線程不是 WAITER 狀態時,這個else if爲真
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) { // 嘗試佔據 WAITER 狀態標識位
waiting = true; // 表明自己正處於 WAITER 狀態,並且讓下一個被用於進入下一個 else if
waiter = Thread.currentThread();
}
}
else if (waiting) // 有線程持有 讀鎖(不會有線程持有 寫鎖),並且當前線程處於 WAITER 狀態時,這個else if爲真
LockSupport.park(this); // 阻塞自己
}
}
// 從根節點開始遍歷查找,找到“相等”的節點就返回它,沒找到就返回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、有線程正持有 寫鎖,這樣做能夠不阻塞讀線程
// 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;
}
else if (U.compareAndSwapInt(this, LOCKSTATE, s, s + READER)) { // 讀線程數量加1,讀狀態進行累加
TreeNode<K,V> r, p;
try {
p = ((r = root) == null ? null : r.findTreeNode(h, k, null));
} finally {
Thread w;
// 如果這是最後一個讀線程,並且有寫線程因爲 讀鎖 而阻塞,那麼要通知它,告訴它可以嘗試獲取寫鎖了
// U.getAndAddInt(this, LOCKSTATE, -READER)這個操作是在更新之後返回lockstate的舊值,
// 不是返回新值,相當於先判斷==,再執行減法
if (U.getAndAddInt(this, LOCKSTATE, -READER) == (READER|WAITER) && (w = waiter) != null)
LockSupport.unpark(w); // 讓被阻塞的寫線程運行起來,重新去嘗試獲取 寫鎖
}
return p;
}
}
}
return null;
}
// 用於實現ConcurrentHashMap.putVal
final TreeNode<K,V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if (p == null) {
first = root = new TreeNode<K,V>(h, k, v, null, null);
break;
}
else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null && (q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null && (q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K,V> x, f = first;
first = x = new TreeNode<K,V>(h, k, v, f, xp);
if (f != null)
f.prev = x;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 下面是有關put加 寫鎖 部分
// 二叉搜索樹新添加的節點,都是取代原來某個的NIL節點(空節點,null節點)的位置
if (!xp.red) // xp是新添加的節點的父節點,如果它是黑色的,新添加一個紅色節點就能夠保證x這部分的一部分路徑關係不變,
// 這是insert重新染色的最最簡單的情況
x.red = true; // 因爲這種情況就是在樹的某個末端添加節點,不會改變樹的整體結構,對讀線程使用紅黑樹搜索的搜索路徑沒影響
else { // 其他情況下會有樹的旋轉的情況出現,當讀線程使用紅黑樹方式進行查找時,可能會因爲樹的旋轉,導致多遍歷、少遍歷節點,影響find的結果
lockRoot(); // 除了那種最最簡單的情況,其餘的都要加 寫鎖,讓讀線程用鏈表方式進行遍歷讀取
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
// 基本是同jdk1.8的HashMap.TreeNode.removeTreeNode,仍然是從鏈表以及紅黑樹上都刪除節點
// 兩點區別:1、返回值,紅黑樹的規模太小時,返回true,調用者再去進行樹->鏈表的轉化;2、紅黑樹規模足夠,不用變換成鏈表時,進行紅黑樹上的刪除要加 寫鎖
final boolean removeTreeNode(TreeNode<K,V> p) {
TreeNode<K,V> next = (TreeNode<K,V>)p.next;
TreeNode<K,V> pred = p.prev; // unlink traversal pointers
TreeNode<K,V> r, rl;
if (pred == null)
first = next;
else
pred.next = next;
if (next != null)
next.prev = pred;
if (first == null) {
root = null;
return true;
}
if ((r = root) == null || r.right == null || (rl = r.left) == null || rl.left == null) // too small
return true;
lockRoot();
try {
TreeNode<K,V> replacement;
TreeNode<K,V> pl = p.left;
TreeNode<K,V> pr = p.right;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
r = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
r = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
root = (p.red) ? r : balanceDeletion(r, replacement);
if (p == replacement) { // detach pointers
TreeNode<K,V> pp;
if ((pp = p.parent) != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
p.parent = null;
}
}
} finally {
unlockRoot();
}
assert checkInvariants(root);
return false;
}
// 下面四個是經典的紅黑樹方法,改編自《算法導論》
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);
// Unsafe相關的初始化工作
private static final sun.misc.Unsafe U;
private static final long LOCKSTATE;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = TreeBin.class;
LOCKSTATE = U.objectFieldOffset(k.getDeclaredField("lockState"));
} catch (Exception e) {
throw new Error(e);
}
}
}
TreeBin的hash值固定爲-2,它是ConcurrentHashMap中用於代理操作TreeNode的特殊節點,持有存儲實際數據的紅黑樹的根節點。因爲紅黑樹進行寫入操作,整個樹的結構可能會有很大的變化,這個對讀線程有很大的影響,所以TreeBin還要維護一個簡單讀寫鎖,這是相對HashMap,這個類新引入這種特殊節點的重要原因。
8. 保留結點-內部類ReservationNode
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null, null);
}
// 空節點代表這個hash桶當前爲null,所以肯定找不到“相等”的節點
Node<K,V> find(int h, Object k) {
return null;
}
}
保留結點也叫空節點,computeIfAbsent和compute這兩個函數式api中才會使用。它的hash值固定爲-3,就是個佔位符,不會保存實際的數據,正常情況是不會出現的,在jdk1.8新的函數式有關的兩個方法computeIfAbsent和compute中才會出現。 爲什麼需要這個節點,因爲正常的寫操作,都會想對hash桶的第一個節點進行加鎖,但是null是不能加鎖,所以就要new一個佔位符出來,放在這個空hash桶中成爲第一個節點,把佔位符當鎖的對象,這樣就能對整個hash桶加鎖了。put/remove不使用ReservationNode是因爲它們都特殊處理了下,並且這種特殊情況實際上還更簡單,put直接使用cas操作,remove直接不操作,都不用加鎖。但是computeIfAbsent和compute這個兩個方法在碰見這種特殊情況時稍微複雜些,代碼多一些,不加鎖不好處理,所以需要ReservationNode來幫助完成對hash桶的加鎖操作。
9. 構造方法
//空的無參構造函數
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 求 2^n
this.sizeCtl = cap; // 用這個重要的變量保存hash桶的接下來的初始化使用的容量
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
// concurrencyLevel只是爲了此方法能夠兼容之前的版本,它並不是實際的併發級別,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); // tableSizeFor,求不小於size的 2^n的算法,jdk1.8的HashMap中說過
this.sizeCtl = cap; // 用這個重要的變量保存hash桶的接下來的初始化使用的容量
// 不進行任何數組(hash桶)的初始化工作,構造方法進行懶初始化lazyInitialization
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
構造方法中都是調用iniTable()方法來構建容器的。
10. iniTable()方法
// 真正的初始化方法,使用保存在sizeCtl中的數據作爲初始化容量
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// Thread.yeild() 和 CAS 都不是100%和預期一致的方法,所以用循環,其他代碼中也有很多這樣的場景
while ((tab = table) == null || tab.length == 0) {
// 看前面sizeCtl這個重要變量的註釋
if ((sc = sizeCtl) < 0)
// 真正的初始化是要禁止併發的,保證tables數組只被初始化一次,但是又不能切換線程,所以用yeild()暫時讓出CPU
Thread.yield();
// CAS更新sizeCtl標識爲 "初始化" 狀態
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) { // 檢查table數組是否已經被初始化,沒初始化就真正初始化
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); // sc = threshold,n - (n >>> 2) = n - n/4 = 0.75n,前面說了loadFactor沒用了,這裏看出,統一用0.75f了
}
} finally {
sizeCtl = sc; // 設置threshold
}
break;
}
}
return tab;
}
三、總結
ConcurrentHashMap幾個重要的點如下:
1. jdk1.8的ConcurrentHashMap不再使用Segment代理Map操作這種設計,整體結構變爲HashMap這種結構,但是依舊保留分段鎖的思想。之前版本是每個Segment都持有一把鎖,1.8版本改爲鎖住恰好裝在一個hash桶本身位置上的節點,也就是hash桶的第一個節點 tabAt(table, i),後面直接叫第一個節點。它可能是Node鏈表的頭結點、保留節點ReservationNode、或者是TreeBin節點(TreeBin節點持有紅黑樹的根節點)。
2. 可以多線程併發來完成擴容這個耗時耗力的操作。在之前的版本中如果Segment正在進行擴容操作,其他寫線程都會被阻塞,jdk1.8改爲一個寫線程觸發了擴容操作,其他寫線程進行寫入操作時,可以幫助它來完成擴容這個耗時的操作。
3. 因爲多線程併發擴容的存在,導致的其他操作的實現上會有比較大的改動,常見的get/put/remove/replace/clear,以及迭代操作,都要考慮併發擴容的影響。
4. 使用新的計數方法。不使用Segment時,如果直接使用一個volatile類變量計數,因爲每次讀寫volatile變量的開銷很大,高併發時效率不如之前版本的使用Segment時的計數方式。jdk1.8新增了一個用與高併發情況的計數工具類java.util.concurrent.atomic.LongAdder,此類是基本思想和1.7及以前的ConcurrentHashMap一樣,使用了一層中間類,叫做Cell(類似Segment這個類)的計數單元,來實現分段計數,最後合併統計一次。因爲不同的計數單元可以承擔不同的線程的計數要求,減少了線程之間的競爭,在1.8的ConcurrentHashMap基本結果改變時,繼續保持和分段計數一樣的併發計數效率。
更多精彩內容,敬請掃描下方二維碼,關注我的微信公衆號【Java覺淺】,獲取第一時間更新哦!