一、爲什麼引入 ConcurrentHashMap 1.8 ?
- JDK 1.7 採用分段鎖思想,整個 Hash 表被分成多個段,每個段中會對應一個 Segment 段鎖,段與段之間可以併發訪問,但是多線程想要操作同一個段是仍需要獲取鎖的。
- JDK 1.8 在控制併發方面則取消了基於 Segment 的分段鎖思想,改用 CAS + synchronized 控制併發操作;在底層數據結構使用 Node 數組+鏈表+紅黑樹,但爲了兼容 jdk1.7,若仍保留了 segment 這個數據結構。
二、源碼閱讀
(1) 底層數據結構
成員變量定義了 ConcurrentHashMap 一些邊界值
// node數組最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認初始值,必須是2的幕數
private static final int DEFAULT_CAPACITY = 16;
//數組可能最大值,需要與toArray()相關方法關聯
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(tranfer時,lc、hc=0兩個計數器分別++記錄原bin、新binTreeNode數量,<=UNTREEIFY_THRESHOLD 則untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大線程數
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中記錄size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED = -1;
// 樹根節點的hash值
static final int TREEBIN = -2;
// ReservationNode的hash值
static final int RESERVED = -3;
// 可用處理器數量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的數組
transient volatile Node<K,V>[] table;
/*控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義
x = 0:默認值
x = -1:代表哈希表正在進行初始化
x < 0:相當於 HashMap 中的 threshold,表示閾值
x < -1:代表有多個線程正在進行擴容*/
private transient volatile int sizeCtl;
再看一下底層數據結構 Node,它實現了 HashMap 中的 Entry,所以 Node 只不過是一個鍵值對。
static class Node<K,V> implements Map.Entry<K,V> {
//鏈表的數據結構
final int hash;
final K key;
//val和next都會在擴容時發生變化,所以加上volatile來保持可見性和禁止重排序
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; }
//不允許更新value
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)));
}
//用於map中的 get() 方法,子類重寫
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 繼承了 Node
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;
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;
}
}
TreeBin 用作樹的頭結點,只存儲 root 和 first 節點,不存儲節點的key、value值。
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; //紅黑樹的根節點
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
//...
}
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;
}
}
(2) 構造方法
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0) // 初始容量小於0,拋出異常
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 找到最接近該容量的2的冪次方數
this.sizeCtl = cap;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m); // 將集合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);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
觀察最後一個構造方法發現新增了一個 sizeCtl 變量,它是怎麼得來的呢?
總結一下:
- 該初始化過程通過指定的初始容量 initialCapacity,加載因子 loadFactor 和預估併發度concurrencyLevel 三個參數計算出一個最小的且大於等於 initialCapacity 大小的 2 的 n 次冪數,即 table 數組的初始大小 sizeCtl
- 若 initialCapacity 爲 15,則 sizeCtl 爲 16
- 但如果 initialCapacity 大小超過了允許的最大值,則 sizeCtl 爲最大值
(3) 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()); // 得到 hash 值
int binCount = 0; // 用於記錄相應鏈表的長度
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果數組"空",進行 Node 數組初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 通過 hash 值算出對應的數組下標,從內存中得到第一個節點 f,並檢查是否爲空 ,如果是再用cas賦值
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 用一次 CAS 操作嘗試將新值 f 放入位置 i
// 如果 CAS 失敗,說明此時有併發操作使得該位置不爲空,那麼直接進入下一次循環
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// 走到這裏,f 就不爲空了;如果整個Map正在擴容
else if ((fh = f.hash) == MOVED)
// 幫助數據遷移,這個等到看完數據遷移部分的介紹後,再理解這個就很簡單了
tab = helpTransfer(tab, f);
else { // 到這裏就是說,f 是該位置的非空頭結點
V oldVal = null;
// 獲取數組該位置的頭結點的同步鎖
synchronized (f) {
if (tabAt(tab, i) == f) {//因爲在獲取鎖的過程中,可能被其他線程改變,所以再次檢查是否等於原值
if (fh >= 0) { // 頭結點的 hash 值大於 0,說明是鏈表
binCount = 1; // 記錄鏈表的長度
// 遍歷鏈表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果發生hash衝突且遇到"相等"的 key,則進行值覆蓋
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;
}
}
} // 紅黑樹,這裏將 TreenNode 改爲 TreeBin 其實是爲了配合 Synchronize,原因如下:
//如果此處的結點爲紅黑樹,如果按照 HashMap 的方式去判斷並插入,會導致此處的頭結點會發生變化,而變化之後鎖住的對象就不是根節點了。
//而如果此處是一個 TreeBin,線程修改的只是 TreeBin 裏面的紅黑樹,無論裏面的樹怎麼改變,此處的鎖亦然不變。
//這種 synchronized (f) {...},寫法在 put 方法中也可見(f是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)
// 如果當前數組的總元素 < 64,那麼會選擇進行數組擴容,而不是轉換爲紅黑樹
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put 方法的篇幅較長,總結一下:
- 根據 key 的 hashcode 值算出 hash 值,遍歷內部的 table,並判斷 tab 是否爲空:
- 如果 tab 爲空,則對 tab 進行初始化
- tab 不空則根據 hash 值得到 tab 的下標 i,拿到獲得該位置頭結點 f,判斷 f 是否爲空:
- 如果 cas 檢查到 f 爲空,則將新對象放到位置 i,並結束 tab 的遍歷。
- 如果
f.hash == MOVED
則表示 此時數組正在擴容,則會去嘗試協助其他線程擴容。 - 如果 f 不空,則嘗試獲取結點 f 的同步鎖:
- 如果拿到鎖,就判斷 f 的類型:
- 如果是鏈表,檢查是否有 hash 衝突和 key 相同的情況,如果
onlyIfAbsent = false
則將舊值進行覆蓋;如果沒有衝突則使用尾插法將結點插入。 - 如果是紅黑樹,則執行紅黑樹的插入方法。
- 如果是鏈表,檢查是否有 hash 衝突和 key 相同的情況,如果
- 如果拿到鎖,就判斷 f 的類型:
- 最後添加結點成功就調用 addCount 方法統計 size,並且在 addCount 方法中檢查是否需要擴容。
- Q:那 put 方法是在哪些方面提供了線程安全保障的呢?如果怎麼保證線程的呢?
- A:時機有:初始化數組時、添加結點是、以及擴容時。
3.1 initTable 方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// thread2在執行下面的compareAndSwapInt方法cas不成功,那麼thread2會到這個分支
// 從而交出 CPU 等待下次系統調度
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 一定是隻有一個線程CAS成功,如果當前線程可以將 sizeCtl 設置爲 -1,代表搶到了機會
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {//有可能在這裏已經別其他線程初始化成功,所以再次判斷一下
if ((tab = table) == null || tab.length == 0) {
// DEFAULT_CAPACITY 默認初始容量是 16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 初始化數組,長度爲 16 或初始化時提供的長度
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 將這個數組賦值給 table,table 是 volatile 的
table = tab = nt;
// 如果 n 爲 16 的話,那麼這裏 sc = 12;n - n/4 = 0.75 × n
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;// 設置 sizeCtl 爲 sc,我們就當是 12 吧
}
break;
}
}
return tab;
}
初始化方法不難,也總結一下:
- 如果 tab 不空就直接返回。
- 如果 tab 爲空,每個併發線程就使用 cas 去競爭初始化機會:
- 如果某一個線程競爭不到機會,就會放棄競爭,自旋至 tab 不空爲止。
- 否則,就對 tab 進行初始化。
注: sizeCtl 默認爲 0,sizeCtl 中記錄 size 大小的偏移量,用來控制 table 的初始化和擴容操作.它的數值有以下含義:
-1
:代表 table 正在初始化,其他線程應該交出 CPU 時間片,退出-N
: 表示正有 N-1 個線程執行擴容操作>0
:表示 tab 已經初始化,代表 tab 容量,默認爲 tab 大小的 0.75 倍,如果還未初始化,代表需要初始化的大小。
按照代碼的執行順序,如果沒有初始化,那麼可能就會進行擴容 transfer,下面就來聊一下擴容...
我們知道在 jdk1.7 中,最大併發量就是就是 seg 的個數,在存在併發操作時,雖然這樣設計使得 seg 對象在擴容時不會影響到其他 seg 對象,但如果該 seg 正在擴容,其他線程還得等到擴容完畢才能對 seg 對象進行讀寫,因此擴容效率就成爲了併發的一個瓶頸;
jdk1.8 就對這個問題進行了一個優化:首先 JDK1.8 去掉了分段鎖,將鎖的級別控制在了更細粒度的 Node 元素級別,同時作者 Doug lea 大神就認爲既然其他線程閒着也是閒着,不如一起參與擴容吧,於是在 jdk1.8 中就引入了一個 ForwardingNode 類以及一個 sizeCtl 來控制 table 的初始化和擴容操
3.2 helpTransfer 方法
我們在 put 方法中發現當 (fh = f.hash) == MOVED
成立時,會進入 helpTransfer 方法中。裏面涉及到了一個ForwardingNode類,先講講它是幹什麼的:
/**
* A node inserted at head of bins during transfer operations.
*/
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;
}
...
}
字面意思是:當整個 table 的某個位置正在進行擴容時,會把一個 ForwardingNode 類型的結點插在 table 的某個 tab[i] 的頭部,注意:並不是某個位置有 ForwardingNode 就表示擴容完全結束了,其他位置可能沒有。
作用不難想到:
- 一個用於連接兩個 table 的節點類。它包含一個 nextTable 指針,用於指向下一張表。而且這個節點的 key value next 指針全部爲n ull,它的 hash 值爲 -1
- 在擴容時,線程 t1 會先判斷該槽點是否爲空:
- 如果爲空,t1 就會將此處的第一個設置爲 forwordingNode,告訴其他線程此位置的數據遷移已有線程包辦。
- 不爲空,則採用頭插法先把此位置的數據給遷移到新的數組中,最後給舊 table 的原位置賦值爲 fwd。
helptransfer參考:https://www.jianshu.com/p/39b747c99d32
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 如果 tab 不空並且結點 f 不是ForwardingNode類型,說明這個位置不在擴容(其他位置可能在擴容)。
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
//若sizeCtl是負數,tab、nextTable和當前table、nextTable相同,說明擴容尚未完成
//因爲一旦擴容完成,就會將cmap的屬性更新
while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) {
// 1.如果sizeCtl>>>16 != rs,則表示標識符變化了
// 2.或者sizeCtl == rs + 1(擴容結束了,不再有線程進行擴容)(默認第一個線程設置sc == (rs左移16位+2),
// 當第一個線程結束擴容,會將 sc 減一。此時sc=rs+1)
// 3.或者sizeCtl == rs + 65535 (如果達到最大幫助線程的數量,即 65535)
// 4.或者transferIndex正在調整 (擴容結束)
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
/* sc的含義
-1:代表 table 正在初始化,其他線程應該交出 CPU 時間片,退出
-N:表示正有 N-1 個線程執行擴容操作
>0:表示 tab 已經初始化,代表 tab 容量,默認爲 tab 大小的 0.75,如果還未初始化,代表需要初始化的大小。*/
// 用CAS嘗試將SIZECTL加1,表示表示增加了一個線程協助擴容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
總結一下 helpTransfer 方法:
- 先判斷當前位置的結點類型是否是 ForwardingNode,和當前結點的 nextTable 是否爲空:
- 如果 f 是 ForwardingNode 類型並且 f 的 nextTable 屬性爲空,則表示 map 不在擴容,返回一個 table 即可(可能是舊數組,也可能是新數組)
- 否則,進一步判斷
this.nextTable
是否和f.nextTab
相等,以及sc=sizeCtl
是否小於 0:- 如果
this.nextTable==f.nextTab
,並且sc < 0
,則表示舊數組正在擴容,爲了保險,再次檢查 4 種代表擴容是否結束的情況是否符合:- 如果擴容結束,break 掉 while 返回即可
- 否則,再次判段
sc
是否與內存中的SIZECTL
相同:- 如果相同,則將用 cas 嘗試將 SIZECTL 加 1,表示表示增加了一個線程協助擴容,然後進入 transfer 方法。
- 否則,回到 while 中再次檢查
this.nextTable==f.nextTab
是否成立,自旋。
- 如果
存疑:
- Q1:在總結的時候,我有一個疑惑,爲什麼一定要用 cas 檢查
SIZECTL
,才能進入 transfer 方法呢?- A1:
- Q2:爲什麼當
sc == rs + 1
就表示 cmap 擴容成功?- A2:這個判斷可以在 addCount 方法中找到答案:默認第一個線程設置
sc == (rs >>> 16) + 2
,當第一個線程結束擴容了,就會將sc--
。此時sc = rs + 1
。
- A2:這個判斷可以在 addCount 方法中找到答案:默認第一個線程設置
如果上面的邏輯都沒問題,接下來就應該看看 cmap 是如何數據遷移的...
3.3 transfer 方法
數據遷移的邏輯比較複雜,因爲它支持併發擴容,而且還沒有加鎖。詳細看看 transfer 方法...
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// stride(步長)在單核下直接等於n,多核模式下爲 (n>>>3)/NCPU,最小值是16
// 一共會有n個位置是需要進行遷移
// n個位置分爲多個區域,每個區域有 stride 個任務
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果 nextTab 爲 null,先進行一次初始化
// 保證只有第一個發起遷移的線程調用此方法時nextTab爲null
if (nextTab == null) {
try {// 容量翻倍
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;//ConcurrentHashMap 中的屬性
transferIndex = n; //同上,用於控制遷移的位置,初始位置爲n
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance 指的是做完一個位置的遷移工作,可以準備做下一個位置的
boolean advance = true; //控制是否要繼續向前掃描
boolean finishing = false; //確保在提交NextTab之前進行掃描,to ensure sweep before committing nextTab
// i是當前掃描位置索引,bound是左邊界,注意是從後往前
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// advance爲true表示需要繼續向區域[bound, transferIndex]掃描
// 簡單理解結局:i指向transferIndex,bound指向 transferIndex-stride
while (advance) {
int nextIndex, nextBound;
// 如果--i>=bound證明當前掃描區域還沒有掃描完,所以沒必要擴張區域。第一次碰到不會執行(i初始爲0)
if (--i >= bound || finishing)
advance = false;
// 如果transferIndex<=0,說明舊數組的所有位置都有線程處理
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false; //退出循環
}//如果cas成功則表示當前線程獲得了一個掃描區域
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; //nextBound 是這次遷移任務的邊界
i = nextIndex - 1;
advance = false; //當前線程已確定掃描區域
}
}//當前線程t進到這裏,表示t"沒事可幹"
if (i < 0 || i >= n || i + n >= nextn) {
int sc; // sizeCtl的別名
if (finishing) { // 如果table的遷移操作已經完成
nextTable = null;
table = nextTab; // 將新的nextTab賦值給table,完成遷移
// 重新計算 sizeCtl:n 是原數組長度,所以 sizeCtl 得出的值將是新數組長度的 0.75倍(4)
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 之前我們說過,sizeCtl 在遷移前會設置爲 (rs << RESIZE_STAMP_SHIFT) + 2
// 然後,每有一個線程參與遷移就會將 sizeCtl 加 1,
// 這裏使用CAS操作對sizeCtl進行減1,代表做完了屬於自己的任務
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 任務結束,方法退出
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 到這裏,說明 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT,
// 也就是說,所有的遷移任務都做完了,也就會進入到上面的 if(finishing){} 分支了
finishing = advance = true;
i = n; // recheck before commit
}
}
// 如果位置i爲空,就用cas插入一個ForwardingNode結點
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 該位置處是一個ForwardingNode(MOVED的本質),代表該位置數據遷移完畢
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {// 獲取鎖並開始處理數組該位置處的遷移工作
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 頭結點的hash值>0,表示此處爲鏈表
if (fh >= 0) {
// 將鏈表一分爲二,
int runBit = fh & n;
Node<K,V> lastRun = f;
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;
}
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);
}
// 低位鏈表放在新數組的位置i
setTabAt(nextTab, i, ln);
// 高位鏈表放在新數組的位置i+n
setTabAt(nextTab, i + n, hn);
// 將原數組該位置處設置爲fwd,標記該位置已經處理完畢,
setTabAt(tab, i, fwd);
// advance 設置爲 true,標記該位置已經遷移完畢
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;
}
}
// 如果低位鏈表的節點數少於 8,則將此處紅黑樹拆成鏈表
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;
// 將 ln 放置在新數組的位置 i
setTabAt(nextTab, i, ln);
// 將 hn 放置在新數組的位置 i+n
setTabAt(nextTab, i + n, hn);
// 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,
// 其他線程看到該位置的 hash 值爲 MOVED,則不進行遷移
setTabAt(tab, i, fwd);
// advance 設置爲 true,代表該位置已經遷移完畢
advance = true; //繼續向前掃描
}
}
}
}
}
}
整理一下思路:視頻 85:00
- ConcurrentHashMap 的擴容支持多線程,在多線程環境下,每一個線程會通過步長計算出自己的負責數據遷移的區域。
- 當線程在某個位置
i
遷移數據時,每一個線程都會鎖住i
位置的頭結點,這樣保證了被鎖的位置不能被其他線程操作。 - 當位置
i
的數據被一個線程遷移後,該位置的鎖將會別釋放掉,並將此處的頭結點設置爲ForwardingNode
,告訴其他線程不必在繼續協助該位置,但沒有ForwardingNode
的結點可以進行 put 操作,最後的數據還是會搬到新數組中, 與此同時,剛剛的線程繼續從右往左掃描,嘗試搬遷其他位置的數據:- 如果有尚未搬遷而且未被其他線程正在搬遷的位置,該線程將會繼續變遷該位置的數據。
- 否則,該線程將會退出本次 transfer 的流程,繼續等待其他線程完成數據遷移。
- 所有數據遷移完全後,擴容完成。
3.3 treeifyBin 方法
如果要進入 treeifyBin 方法後判斷是否要進行樹化
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// MIN_TREEIFY_CAPACITY = 64
// 如果數組長度小於 64 的時候,其實也就是 8 < x < 64 時會對數組擴容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
// 後面我們再詳細分析這個方法
tryPresize(n << 1);
// 如果 b 是非空頭結點
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));
}
}
}
}
}
3.4 addCount 方法
addCount 方法不容易理解,簡單說一下它的作用吧:
- 對 table 的總元素個數加一。無論是通過修改 baseCount,還是通過使用 CounterCell。當 CounterCell 被初始化了,就優先使用 CounterCell,不再使用 baseCount。
- 檢查是否需要擴容,或者是否正在擴容。如果需要擴容,就調用擴容方法,如果正在擴容,就幫助其擴容。
對應視頻 JDK8中ConcurrentHashMap源碼解析(上) 的 65分鐘
(4) get 方法
get 方法相對比較簡單
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//計算hash值
int h = spread(key.hashCode());
//根據hash值確定節點位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果搜索到的節點key與傳入的key相同且不爲null,直接返回這個節點
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果eh<0 說明這個節點在樹上 直接尋找
else if (eh < 0)
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 值,然後再算出桶下標
i
- 判斷
i
位置是否有元素:- 如果沒有元素,返回 null
- 否則,如果 hash 值與 key 都相同,那麼返回當前位置元素的 val
- 如果 hash 值與 key 都不相同,那麼在此檢查位置
i
的結點類型:- 如果是紅黑樹就調用紅黑樹的查找方法
- 如果是鏈表就遍歷鏈表找出對應元素。
(5) size 方法
詳講每個方法,包括 UNsafe:https://blog.csdn.net/u010723709/article/details/48007881
addCount:https://www.jianshu.com/p/749d1b8db066 https://www.cnblogs.com/dgutfly/p/11425599.html
開源中國:https://my.oschina.net/hosee/blog/675884#h2_10 https://www.javadoop.com/post/hashmap#toc_11
https://blog.csdn.net/Bill_Xiang_/article/details/81122044
有目錄那個博客園:https://www.cnblogs.com/study-everyday/p/6430462.html#autoid-2-1-4
helptransfer、以及sizeCtl 參考:https://www.jianshu.com/p/39b747c99d32
代碼很多註釋:https://www.cnblogs.com/zerotomax/p/8687425.html#go2
講了finnish的怎麼得到:https://www.cnblogs.com/yangming1996/p/8031199.html