目錄
什麼是ConcurrentHashMap
ConcurrentHashMap是J.U.C包裏面提供的一個線程安全且高效的HashMap。在併發場景中shi使用頻率比較高。由於是HashMap的派生類,所以其API和HashMap類似。
ConcurrentHashMap源碼分析
JDK1.7和JDK1.8版本的變化
在JDK1.7的實現上,ConcurrentHashMap是一個Segment數組。通過繼承ReentrantLock來進行加鎖。通過每次鎖住一個segment來保證每個segment內操作的線程安全行從而實現全局的線程安全。
相比於JDK1.7版本,JDK1.8主要有兩個改進。
1.取消了segment分段設計,直接使用Node數組來保存數據。並且採用Node數組元素作爲鎖來實現每一行數據進行加鎖來進一步減少併發衝突的概率。
2.將原本數組+單向鏈表的數據結構變更爲了數組+單向鏈表+紅黑樹的數據結構。
爲什麼要引入紅黑樹?
理想情況下,key hash能夠均勻的分散在數組中,那麼數組中每個隊列的長度主要爲0或1.實際情況下,會存在一些隊列過長的情況。如果還採用單向鏈表方式,那麼查詢某個節點的時間複雜度就變爲0(n);因此對於隊列長度超過8的鏈表,JDK1.8採用紅黑樹紅黑樹的結構,查詢複雜度爲o(logN)。提高了查詢性能。(這個結構與HashMap的實現結構基本一致)
從put方法切入
調用ConcurrentHashMap的put方法-自旋操作插入數據(解決併發場景)判斷邏輯爲
第一個if判斷主要對數組進行初始化。
第二個if判斷:通過元素的hash值,得到數組對應的位置下標,讀取目標位置中的元素,如果爲空則通過cas操作進行數據插入。插入成功,跳出循環。
第三個if判斷:判斷是否需要擴容 MOVED:擴容標識
第四塊else代碼塊:當數組已經被初始化,且當前位置有數據,並且還不需要擴容的情況下。這個時候會在當前位置追加元素,形成鏈表。這裏有synchronized塊,鎖對象是當前位置的頭節點。保證了併發安全。
注意點:多個線程同時進入第二個if判斷的時候,假如有線程A成功執行casTabAt操作後。其它線程可以立即看到table[i]的變化。原因:casTabAt方法具有與volatile讀寫相同的內存語義,根據volatile的happens-before原則:線程A的casTabAt操作對其它線程可見。
總結
1.ConcurrentHashMap向數組內插入數據並未採用加鎖的方式。當ConcurrentHashMap調用put方法插入數據的時候,會首先採用volatile讀的方式,讀取目標數組位置的元素,以保證每次讀取到的值都是最新的。如果讀取到的元素爲空,則通過cas操作將新的值封裝成node插入。
2.ConcurrentHashMap向某個數組位置的鏈表插入數據的時候,會對該位置的頭節點進行加鎖,鎖的粒度更細。同時不影響數組中其它位置數據的插入。這就保證了在單個節點鏈表上的數據增加是線程安全的。其它節點同時也可以併發插入數據。體現了分佈式思想,達到了性能上的提升。
注意點:當鏈表的長度大於等於8時,會判斷是否將鏈表轉化爲紅黑樹。所以,在同步代碼塊邏輯中會有判斷是否將數據插入到紅黑樹的邏輯。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 計算hash值
int hash = spread(key.hashCode());
// 用來記錄鏈表的長度
int binCount = 0;
// 自旋操作,當出現線程競爭時不斷自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果數組爲空則對數組進行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 通過hash值對應的數組下標得到第一個節點;以volatile讀的方式來讀取table數組中的元素
// 保證每次拿到的數據都是最新的
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果下標返回的節點爲空,則通過cas操作將新的值封裝成node插入即可,如果cas失敗,說明存在競爭,則進入下一次循環
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 跳過前幾個判斷,代碼執行到此處,表示當前下標已經有元素了
V oldVal = null;
// 對鎖的頭節點進行加鎖
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 當鏈表長度 大於等於 8 之後 判斷是否進行鏈轉紅黑樹
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put方法-初始化數組 initTable()
初始化數組方法中,有一個屬性值 sizeCtrl 該屬性值有三個作用
1.當該屬性值等於 -1 即小於零,表示當前有線程正在對數組進行初始化或者擴容
2.當該屬性值大於0的時候,用於去判斷下一次是否需要擴容。
3.爲負數且不是-1的時候,用來去判斷有多少個線程正在擴容(-2 有一個線程正在擴容,以此類推)
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 這裏判斷 sizeCtl的值是否小於零
// 如果小於零則表示當前已有線程搶佔了初始化操作,直接讓出自己的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,或者初始化在構造ConcurrentHashMap的時候傳入的長度
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 將初始化完成的數組賦值給 table
table = tab = nt;
// 計算當前容量的0.75倍
sc = n - (n >>> 2);
}
} finally {
// 設置 sizeCtl 爲 sc 如果默認16,這個時候 sc = 12
// 即當前容量的0.75倍
sizeCtl = sc;
}
break;
}
}
return tab;
}
put方法 - tabAt
tabAt方法本質上就是去獲取tab[i]的值。通過getObjectVolatile方法去獲取。而不是直接tab[i]的方式獲取。
原因:tabAt方法是支持在併發場景中使用的代碼。getObjectVolatile方法的保證了可見性,通過Unsafe類對tab進行操作同時又保證了性能。
總結:以volatile讀的形式去讀取tab[i]的值。若爲空,則通過cas操作把新的數據封裝成爲node節點插入到數組中去。cas操作保證了數據的更新對volatile讀可見,從而保證了併發場景下的線程安全。
擴展-偏移地址:偏移地址應用在數組中,聲明一個數組的時候,棧空間存儲數組名,但是沒有內存地址。當對數組進行實例化之後,JVM會在堆空間中分配一塊連續的空間來保存數組。JVM不會給數組中的每一個元素分配地址。只會給數組分配一個首地址。然後棧空間的數組名指向這個首地址。數組中每一個元素的地址實際上是相對數組首地址的偏移量。即數據元素是以偏移地址的形式訪問的。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
// 從對象 tab 即我們的數組,指定 偏移地址 處獲取變量的引用
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
第二個if判斷本質上是把數據插入到集合中去,如果插入成功則跳出循環。失敗則執行循環內後續代碼。
put方法-數據插入成功
數據插入成功之後,最終會執行addCount方法。該方法的主要功能是計數,計算集合內元素個數。
addCount方法這裏主要做了兩件事
1.計數,通過邏輯判斷採用併發場景下計數或非併發場景下計數。
總結:ConcurrentHashMap數據插入成功之後會調用addCount方法進行計數,當沒有競爭的時候,通過cas操作BASECOUNT變量進行元素個數統計。當在併發場景下的時候。使用CounterCell數組來記錄元素個數。
2.擴容階段
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 首先判斷 counterCells 是否爲空
// 1.如果爲空,通過 cas 操作嘗試修改 baseCount 變量,對該變量進行原子累加操作。
// 意義:若無競爭,採用 baseCount 來記錄元素個數
// 2.如果 cas 失敗,說明存在競爭,這個時候通過 CounterCell 數組 來記錄
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
// 是否衝突標識,默認沒有衝突
boolean uncontended = true;
// 一共有四個判斷
// 1.2 如果 CounterCell 數組爲空或長度,直接調用 fullAddCount
// 3. 從數組中 取出一個元素的位置數據爲空, 直接調用 fullAddCount
// 4. 通過cas操作修改CounterCell隨機位置的值,如果修改失敗說明存在併發 調用fullAddCount
// CounterCell數組中的元素用來記錄元素個數,如果cas成功,則 v+x 的值更新到數組中去,v:當前元素個數 x:新增元素個數
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);
return;
}
if (check <= 1)
return;
s = sumCount();
}
......
}
CounterCell數組,通過分析代碼。在併發場景下,ConcurrentHashMap是通過CounterCell數組來記錄元素個數的。
爲什麼要使用數組來記錄元素個數?
原因:ConcurrentHashMap是併發集合,在併發場景下,單個變量統計元素個數勢必會影響程序性能。所以,使用數組來記錄元素個數。體現了“分而治之”的思想,即分片記錄元素個數。
CounterCell數組中的每個元素,都存儲着一個元素個數。sumCount方法通過循環累加來得到集合的大小。
private transient volatile CounterCell[] counterCells;
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
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;
}
fullAddCount方法
fullAddCount方法在addCount方法中最終會被調用。fullAddCount方法體現了“分而治之”的思想。
主要用來初始化CounterCell數組,記錄元素個數,擴容等操作
提出問題:ConcurrentHashMap是如何記錄元素個數的?
Answer:在單線程的情況下,ConcurrentHashMap採用CAS操作全局變量的方式統計元素個數。在併發場景下,採用數組存儲元素個數的形式去記錄。具體做法:每一個線程會獲取一個隨機數,各個線程擁有的隨機數不相同。通過計算,每一個線程可以同時去操作不同的數組位置,把各自處理的元素個數存儲到對應的位置。體現了“分而治之”的思想。當線程數量較多時,會對記錄元素個數的數組進行擴容,數組長度默認爲2,每次擴容一倍。
擴展:如何保證各個線程擁有不同的隨機數?
private final void fullAddCount(long x, boolean wasUncontended) {
int h;
// 獲取當前線程的 probo 的值,即隨機數 如果爲0 進行初始化
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
// 重新獲取隨機數
h = ThreadLocalRandom.getProbe();
// 重新設置了隨機數,衝突標誌位設置爲 true
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;;) { // 自旋
CounterCell[] as; CounterCell a; int n; long v;
// 判斷 counterCells 是否已經進行初始化
if ((as = counterCells) != null && (n = as.length) > 0) {
// 當前數組長度減一 和 當前線程的隨機數 進行 & 運算,
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
// 構造一個 CounterCell 元素,傳入元素個數
CounterCell r = new CounterCell(x); // Optimistic create
// 通過cas操作設置標識位 爲 1 防止其它線程對 counterCells 做併發處理
if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean created = false;
try { // Recheck under lock
// 將初始化的 CounterCell 對象 r 放到數組對應下標位置
CounterCell[] rs; int m, j;
if ((rs = counterCells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
// 創建成功退出循環,cells下標位置數據不爲空,進行下一次循環。
continue; // Slot is now non-empty
}
}
collide = false;
}
// 表示 在 addCount 方法中 cas 失敗了,且獲取的 probe 不爲空
else if (!wasUncontended) // CAS already known to fail
// 重置衝突標識,進行下一次自旋
wasUncontended = true; // Continue after rehash
// 指定下標位置的 cell 不爲空, 通過 cas 操作進行原子累加,如果成功,直接退出
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
break;
// 兩個判斷 1.是否有其它線程建立了新的 counterCells
// 2.counterCells的大小,大於cpu的核心數(線程的併發數不會大於cpu的核心數)
else if (counterCells != as || n >= NCPU)
// 設置當前循環失敗,,不進行擴容
collide = false; // At max size or stale
// 恢復狀態 下一次進行擴容
else if (!collide)
collide = true;
// 進行擴容
else if (cellsBusy == 0 &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
try {
if (counterCells == as) {// Expand table unless stale
// 擴容一倍 2->4
CounterCell[] rs = new CounterCell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
counterCells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
// 更新隨機數的值
h = ThreadLocalRandom.advanceProbe(h);
}
// 初始化 CounterCell 數組
// cellsBusy : 標識位 0 表示沒有在做初始化,通過cas更新cellsBusy的值,1 標識 當前線程正在做初始化操作
else if (cellsBusy == 0 && counterCells == as &&
U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
boolean init = false;
try { // Initialize table
if (counterCells == as) {
// 數組的初始化長度爲2
CounterCell[] rs = new CounterCell[2];
// 把元素個數 x 放在指定的數組下標位置
rs[h & 1] = new CounterCell(x);
counterCells = rs;
init = true;
}
} finally {
// 恢復標識
cellsBusy = 0;
}
if (init)
break;
}
// 併發場景下,競爭激烈,直接累加元素個數到 BASECOUNT 變量中
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
break; // Fall back on using base
}
}
ConcurrentHashMap擴容階段
addCount方法第二階段
首先要判斷是否需要擴容,判斷邏輯爲當前數組大小是否大於自身容量的0.75倍。且數組容量小於最大容量。證明此時數組可以進行擴容.。
// binCount >= 0 鏈表長度總是大於等於0 檢查是否需要擴容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
// s 表示集合大小 如果集合大小 大於等於 擴容閾值(當前容量的0.75倍) 並且table容量不爲空且長度小於最大容量
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 生成一個擴容戳
int rs = resizeStamp(n);
// sc < 0 即 sizeCtrl < 0 說明已經有別的線程正在擴容了
if (sc < 0) {
// 5個判斷 true:表示當前線程不能幫助此次擴容,直接跳出循環
// 1. sc >>> RESIZE_STAMP_SHIFT!=rs 表示比較高 RESIZE_STAMP_BITS 位
// 生成戳和 rs 是否相等
// 2. sc=rs+1 表示擴容結束
// 3. sc==rs+MAX_RESIZERS 表示幫助線程線程已經達到最大值了
// 4. nt=nextTable -> 表示擴容已經結束
// 5. transferIndex<=0 表示所有的 transfer 任務都被領取完了,沒有剩餘的
// hash 桶來給自己自己好這個線程來做 transfer
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 當前線程嘗試幫助擴容,成功則,調用 transfer 方法進行擴容
// sc+1 表示增加一個線程去幫助進行擴容操作。
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 如果當前沒有在擴容,那麼 rs 肯定是一個正數,通過 rs<<RESIZE_STAMP_SHIFT
// 將sc 設置爲一個負數,+2 表示有一個線程在執行擴容
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
// 重新計數
s = sumCount();
}
}
在while循環條件中,sizeCtl的值被賦值給變量sc 即如果滿足擴容條件會首先執行如下代碼。首先讓一個線程去執行擴容操作。
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
擴容戳-resizeStamp方法
Integer.numberOfLeadingZeros(n)返回了一個無符號整數n的最高非零位前面0的個數。
10的2進制數是 0000 0000 0000 0000 0000 0000 0000 1010 返回28
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
假定 n = 16 resizeStamp(16) = 32795
轉化爲二進制是
【0000 0000 0000 0000 1000 0000 0001 1011】
當第一個線程嘗試進行擴容會執行如下代碼
U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2);
rs左移16位變爲 【1000 0000 0001 1011 0000 0000 0000 0000】
然後再+2變爲 【1000 0000 0001 1011 0000 0000 0000 0010】
結論:高16位代表擴容的標記 低16位代表並行擴容的線程數
第一次擴容爲什麼+2?
原因:1表示初始化,2表示一個線程正在擴容,對sizeCtrl的操作都是基於位運算的,所以不會關心具體數值是多少,只關心其在二進制上的數值。
transfer-多線程下的擴容
CAS實現無鎖的併發同步策略,利用多線程來進行協同擴容
關鍵點1:當前數組長度右移三位除以CPU核心線程數,是否小於16.目的是平均爲CPU分配任務,讓每一個CPU可以處理至少16個長度的數組,以達到平均分配的效果。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
transfer方法源碼註釋版
問題:ConcurrentHashMap如何實現擴容?
Answer:ConcurrentHashMap支持併發擴容,實現方式是把node數組進行拆分,讓每個線程處理自己的區域。拆分數組的方式首先是通過CPU核心線程數計算出每個線程適合處理的數組長度。
假如一個容量爲32的ConcurrentHashMap集合需要進行擴容,參與擴容的每一個線程適合處理的數組長度爲16.
計算好每個數組處理的數組長度後,開始給每一個線程分配具體的數據遷移任務。容量爲32的集合進行擴容,一個線程適合處理的數組長度是16。此時假如線程A和B2個線程並行擴容。最終會爲線程A分配0-15位置的數組進行處理,線程B分配16-31位置的數組進行處理。從而實現並行擴容。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 計算結果 小於 16 則直接賦值爲 16
// 目的是爲了讓每一個CPU的任務儘量均勻,默認一個CPU處理 16 個 bucket
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// nextTab : 用來擴容的數組,未初始化
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
// 新建一個原始table 2倍大小的 nt 賦值給 nextTab
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
// 把新建的 table 賦值給入參 nextTab (入參爲 null 的情況下)
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
// 擴容失敗 sizeCtrl 使用 int 的最大值
sizeCtl = Integer.MAX_VALUE;
return;
}
// 把新建的 table 再賦值給全局變量 nextTable
nextTable = nextTab;
// 把數組長度 賦值給 transferIndex
transferIndex = n;
}
// nextn : 新的 table 長度
int nextn = nextTab.length;
// 創建一個 fwd 節點,表示一個正在被遷移的Node 並且 hash 值爲 -1(MOVED) 標識正在遷移
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
// 判斷是否已經完成了擴容
boolean finishing = false; // to ensure sweep before committing nextTab
// 通過for自循環處理每個槽位中的鏈表元素 advance 默認爲 true
// 通過 CAS 設置 transferIndex 屬性值,並初始化 i 和 bound 值
// i 指當前處理的槽位序號, bound 指需要處理的槽位邊界,先處理槽位 15 的節點
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 循環使用 CAS 不斷嘗試爲當前線程分配任務,直到分配成功或任務隊列已經被全部分配完畢
// 如果當前線程已經被分配過 bucket 區域 那麼會通過--i 指向下一個待處理 bucket 然後退出該循環
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
// transferIndex <= 0 表示沒有需要處理的 bucket 了
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 假如對數組容量爲 32 的目標數組進行擴容,那麼此時nextIndex的值爲32 stride的值爲16
// 所以計算得出 nextBound的值爲 16 即更新 transferIndex 的值爲16
// nextBound = (nextIndex > stride ? nextIndex - stride : 0)
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 此時 bound = 16
bound = nextBound;
// i = 31
i = nextIndex - 1;
advance = false;
}
}
// i 表示目前處理數組下標節點 這裏三個判斷是用來判斷是否完成了數據遷移
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
// 如果完成了擴容 刪除成員變量
nextTable = null;
// 更新 table 數組
table = nextTab;
// 更新閾值 16->12 32->24
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// CAS 操作,對 sizeCtl 變量的低16位進行減一操作,表示一個線程完成了屬於自己的數據遷移, 相當於對數據進行復位
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 前面有 sc+2 的操作,這裏 判斷如果相等了,表明已經完成了擴容操作
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
// 如果相等,擴容結束了,更新 finising 變量
finishing = advance = true;
// 再次循環檢查一下整張表
i = n; // recheck before commit
}
}
// 如果位置 i 處是空的,沒有任何節點,
// 那麼放入剛剛初始化的 ForwardingNode ”空節點“
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
// 目標節點狀態爲 MOVED 表明當前節點已經被處理過了,其它線程不再進行處理
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 具體的數據遷移過程
.....
}
}