一.背景
前文講了HashMap的原始碼分析,從中可以看到下面的問題:
- HashMap的put/remove方法不是執行緒安全的,如果在多執行緒併發環境下,使用synchronized進行加鎖,會導致效率低下;
- 在遍歷迭代獲取時進行修改(put/remove)操作,會導致發生併發修改異常(ConcurrentModificationException);
- 在JDK1.7之前,對HashMap進行put新增操作,會導致連結串列反轉,造成連結串列迴路,從而發生get死迴圈,(當然這個問題在JDK1.8被改進了按照原連結串列順序進行重排移動);
- 如果多個執行緒同時檢測到元素個數超過 陣列大小 * loadFactor,這樣就會發生多個執行緒同時對陣列進行擴容,都在重新計算元素位置以及複製資料,但是最終只有一個執行緒擴容後的陣列會賦給 table,也就是說其他執行緒的都會丟失,並且各自執行緒 put 的資料也丟失;
基於上述問題,都可以使用ConcurrentHashMap進行解決,ConcurrentHashMap使用分段鎖技術解決了併發訪問效率,在遍歷迭代獲取時進行修改操作也不會發生併發修改異常等等問題。
二.原始碼解析
-
構造方法:
//最大容量大小 private static final int MAXIMUM_CAPACITY = 1 << 30; //預設容量大小 private static final int DEFAULT_CAPACITY = 16; /** *控制識別符號,用來控制table的初始化和擴容的操作,不同的值有不同的含義 * 多執行緒之間,以volatile方式讀取sizeCtl屬性,來判斷ConcurrentHashMap當前所處的狀態。 * 通過cas設定sizeCtl屬性,告知其他執行緒ConcurrentHashMap的狀態變更 *未初始化: * sizeCtl=0:表示沒有指定初始容量。 * sizeCtl>0:表示初始容量。 *初始化中: * sizeCtl=-1,標記作用,告知其他執行緒,正在初始化 *正常狀態: * sizeCtl=0.75n ,擴容閾值 *擴容中: * sizeCtl < 0 : 表示有其他執行緒正在執行擴容 * sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此時只有一個執行緒在執行擴容 */ private transient volatile int sizeCtl; //併發級別 private static final int DEFAULT_CONCURRENCY_LEVEL = 16; //建立一個新的空map,預設大小是16 public ConcurrentHashMap() { } public ConcurrentHashMap(int initialCapacity) { if (initialCapacity < 0) throw new IllegalArgumentException(); //調整table的大小,tableSizeFor的實現檢視前文HashMap原始碼分析的構造方法模組 int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); this.sizeCtl = cap; } public ConcurrentHashMap(Map<? extends K, ? extends V> m) { this.sizeCtl = DEFAULT_CAPACITY; putAll(m); } public ConcurrentHashMap(int initialCapacity, float loadFactor) { this(initialCapacity, loadFactor, 1); } /** * concurrencyLevel:併發度,預估同時運算元據的執行緒數量 * 表示能夠同時更新ConccurentHashMap且不產生鎖競爭的最大執行緒數。 * 預設值為16,(即允許16個執行緒併發可能不會產生競爭)。 */ 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; }
-
put:
public V put(K key, V value) { return putVal(key, value, false); } static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash普通節點雜湊的可用位 //把位數控制在int最大整數之內,h ^ (h >>> 16)的含義檢視前文的put原始碼解析 static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; } final V putVal(K key, V value, boolean onlyIfAbsent) { //key和value為空丟擲異常 if (key == null || value == null) throw new NullPointerException(); //得到hash值 int hash = spread(key.hashCode()); int binCount = 0; //自旋對table進行遍歷 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //初始化table if (tab == null || (n = tab.length) == 0) tab = initTable(); //如果hash計算出的槽位元素為null,CAS將元素填充進當前槽位並結束遍歷 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 } //hash為-1,說明正在擴容,那麼就幫助其擴容。以加快速度 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; synchronized (f) {// 同步 f 節點,防止增加連結串列的時候導致連結串列成環 if (tabAt(tab, i) == f) {// 如果對應的下標位置的節點沒有改變 if (fh >= 0) {//f節點的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; //hash和key不同,新增到連結串列後面 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; }
-
initTable:初始化
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { //如果一個執行緒發現sizeCtl<0,意味著另外的執行緒執行CAS操作成功,當前執行緒只需要讓出cpu時間片,即保證只有一個執行緒初始化 //由於sizeCtl是volatile的,保證了順序性和可見性 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//cas操作判斷並置為-1 try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//若沒有引數則預設容量為16 @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//建立陣列 table = tab = nt;//陣列賦值給當前ConcurrentHashMap //計算下一次元素到達擴容的閥值,如果n為16的話,那麼這裡 sc = 12,其實就是 0.75 * n sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
-
tabAt:尋找指定陣列在記憶體中i位置的資料
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { /**getObjectVolatile:獲取obj物件中offset偏移地址對應的object型field的值,支援volatile load語義。 * 陣列的定址計算方式:a[i]_address = base_address + i * data_type_size * base_address:起始地址;i:索引;data_type_size:資料型別長度大小 */ return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); }
-
helpTransfer:幫助擴容
private static int RESIZE_STAMP_BITS = 16; /** * numberOfLeadingZeros()的具體演算法邏輯請參考:https://www.jianshu.com/p/2c1be41f6e59 * numberOfLeadingZeros(n)返回的是n的二進位制標識的從高位開始到第一個非0的數字的之間0的個數,比如numberOfLeadingZeros(8)返回的就是28 ,因為0000 0000 0000 0000 0000 0000 0000 1000在1前面有28個0 * RESIZE_STAMP_BITS 的值是16,1 << (RESIZE_STAMP_BITS - 1)就是將1左移位15位,0000 0000 0000 0000 1000 0000 0000 0000 * 然後將兩個數字再按位或,將相當於 將移位後的 兩個數相加。 * 比如: * 8的二進位制表示是: 0000 0000 0000 0000 0000 0000 0000 1000 = 8 * 7的二進位制表示是: 0000 0000 0000 0000 0000 0000 0000 0111 = 7 * 按位或的結果是: 0000 0000 0000 0000 0000 0000 0000 1111 = 15 * 相當於 8 + 7 =15 * 為什麼會出現這種效果呢?因為8是2的整數次冪,也就是說8的二進位制表示只會在某個高位上是1,其餘地位都是0,所以在按位或的時候,低位表示的全是7的位值,所以出現了這種效果。 */ static final int resizeStamp(int n) { return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); } final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; //如果table不是空,且node節點是轉移型別,且node節點的nextTable(新 table)不是空,嘗試幫助擴容 if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { //根據length得到一個識別符號號 int rs = resizeStamp(tab.length); //如果nextTab沒有被併發修改,且tab也沒有被併發修改,且sizeCtl<0(說明還在擴容) while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { /** * 如果 sizeCtl 無符號右移16不等於rs( sc前16位如果不等於識別符號,則識別符號變化了) * 或者 sizeCtl == rs + 1(擴容結束了,不再有執行緒進行擴容)(預設第一個執行緒設定 sc ==rs 左移 16 位 + 2,當第一個執行緒結束擴容了,就會將 sc 減1。這個時候,sc 就等於 rs + 1) * 或者 sizeCtl == rs + 65535 (如果達到最大幫助執行緒的數量,即 65535) * 或者轉移下標正在調整 (擴容結束) * 結束迴圈,返回 table * 【即如果還在擴容,判斷識別符號是否變化,判斷擴容是否結束,判斷是否達到最大執行緒數,判斷擴容轉移下標是否在調整(擴容結束),如果滿足任意條件,結束迴圈。】 */ if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; // 如果以上都不是, 將 sizeCtl + 1, (表示增加了一個執行緒幫助其擴容) if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { transfer(tab, nextTab);//進行擴容和資料遷移 break; } } return nextTab;//返回擴容後的陣列 } return table;//沒有擴容,返回原陣列 }
-
transfer:擴容和資料遷移,採用多執行緒擴容,整個擴容過程,通過cas設定sizeCtl、transferIndex等變數協調多個執行緒進行併發擴容;
-
transferIndex屬性:
//擴容索引,表示已經分配給擴容執行緒的table陣列索引位置。主要用來協調多個執行緒,併發安全地獲取遷移任務(hash桶)。 private transient volatile int transferIndex;
-
在擴容之前,transferIndex 在陣列的最右邊 。此時有一個執行緒發現已經到達擴容閾值,準備開始擴容。
-
擴容執行緒,在遷移資料之前,首先要將transferIndex左移(以cas的方式修改 transferIndex=transferIndex-stride(要遷移hash桶的個數)),獲取遷移任務。每個擴容執行緒都會通過for迴圈+CAS的方式設定transferIndex,因此可以確保多執行緒擴容的併發安全。(換個角度,我們可以將待遷移的table陣列,看成一個任務佇列,transferIndex看成任務佇列的頭指標。而擴容執行緒,就是這個佇列的消費者。擴容執行緒通過CAS設定transferIndex索引的過程,就是消費者從任務佇列中獲取任務的過程。
-
-
擴容過程:
-
容量已經達到擴容閾值,需要進行擴容操作,此時transferindex=tab.length=32
-
擴容執行緒A 以cas的方式修改transferindex=31-16=16 ,然後按照降序遷移table[31]--table[16]這個區間的hash桶
-
遷移hash桶時,會將桶內的連結串列或者紅黑樹,按照一定演算法,拆分成2份,將其插入nextTable[i]和nextTable[i+n](n是table陣列的長度)。 遷移完畢的hash桶,會被設定成ForwardingNode節點,以此告知訪問此桶的其他執行緒,此節點已經遷移完畢
-
此時執行緒2訪問到了ForwardingNode節點,如果執行緒2執行的put或remove等寫操作,那麼就會先幫其擴容。如果執行緒2執行的是get等讀方法,則會呼叫ForwardingNode的find方法,去nextTable裡面查詢相關元素
-
執行緒2加入擴容操作
-
如果準備加入擴容的執行緒,發現以下情況,放棄擴容,直接返回。
-
發現transferIndex=0,即所有node均已分配
-
發現擴容執行緒已經達到最大擴容執行緒數
-
-
-
-
-
原始碼解析
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { int n = tab.length, stride; //先判斷CPU核數,如果是多核,將陣列長度/8,再/核數,得到stride,否則stride=陣列長度,如果stride<16,則stride=16 //這裡的目的是讓每個CPU處理的桶一樣多,避免出現轉移任務不均勻的現象,如果桶較少的話,預設一個CPU(一個執行緒)處理16個桶,即確保每次至少獲取16個桶(遷移任務) if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range //未初始化進行初始化 if (nextTab == null) { // initiating try { @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//擴容2倍 nextTab = nt;//更新 } catch (Throwable ex) { // try to cope with OOME sizeCtl = Integer.MAX_VALUE;//擴容失敗,sizeCtl使用int最大值。 return; } nextTable = nextTab;//更新成員變數 //transferIndex預設=table.length transferIndex = n; } int nextn = nextTab.length;//新tab的長度 //建立一個fwd節點,用於佔位。當別的執行緒發現這個槽位中是fwd型別的節點,表示其他執行緒正在擴容,並且此節點已經擴容完畢,跳過這個節點。關聯了nextTab,可以通過ForwardingNode.find()訪問已經遷移到nextTab的資料。 ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); //首次推進為 true,如果等於true,說明需要再次推進一個下標(i--),反之,如果是false,那麼就不能推進下標,需要將當前的下標處理完畢才能繼續推進 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; //while:如果當前執行緒可以向後推進;這個迴圈就是控制i遞減。同時,每個執行緒都會進入這裡取得自己需要轉移的桶的區間 //分析場景:table.length=32,此時執行到這個地方nextTab.length=64 A,B執行緒同時進行擴容。 //A,B執行緒同時執行到while迴圈中cas這段程式碼 //A執行緒獲第一時間搶到資源,設定bound=nextBound=16,i = nextIndex - 1=31 A執行緒搬運table[31]~table[16]中間16個元素 //B執行緒再次回到while起點,然後在次獲取到 bound = nextBound-0,i=nextIndex - 1=15,B執行緒搬運table[15]~table[0]中間16個元素 //當transferIndex=0的時候,說明table裡面所有搬運任務都已經完成,無法在分配任務。 while (advance) { int nextIndex, nextBound; // 對i減1,判斷是否大於等於bound(正常情況下,如果大於bound不成立,說明該執行緒上次領取的任務已經完成了。那麼,需要在下面繼續領取任務) // 如果對i減1大於等於 bound,或者完成了,修改推進狀態為 false,不能推進了。任務成功後修改推進狀態為 true。 // 通常,第一次進入迴圈,i-- 這個判斷會無法通過,從而走下面的nextIndex = transferIndex(獲取最新的轉移下標)。其餘情況都是:如果可以推進,將i減1,然後修改成不可推進。如果i對應的桶處理成功了,改成可以推進。 if (--i >= bound || finishing) advance = false;//這裡設定false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進 // 這裡的目的是:1. 當一個執行緒進入時,會選取最新的轉移下標。 // 2. 當一個執行緒處理完自己的區間時,如果還有剩餘區間的沒有別的執行緒處理,再次CAS獲取區間。 else if ((nextIndex = transferIndex) <= 0) { // 如果小於等於0,說明沒有區間可以獲取了,i改成-1,推進狀態變成false,不再推進 // 這個-1會在下面的if塊裡判斷,從而進入完成狀態判斷 i = -1; advance = false;//這裡設定false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進 } // CAS修改transferIndex,即 length - 區間值,留下剩餘的區間值供後面的執行緒使用 else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound;//這個值就是當前執行緒可以處理的最小當前區間最小下標 i = nextIndex - 1;//初次對i賦值,這個就是當前執行緒可以處理的當前區間的最大下標 advance = false;// 這裡設定false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進,這樣導致漏掉某個桶。下面的 if(tabAt(tab, i) == f) 判斷會出現這樣的情況。 } } //i<0(不在 tab 下標內,按照上面的判斷,領取最後一段區間的執行緒結束) if (i < 0 || i >= n || i + n >= nextn) { int sc; if (finishing) {// 如果完成了擴容和資料遷移 nextTable = null;//刪除成員遍歷 table = nextTab;//更新table sizeCtl = (n << 1) - (n >>> 1);//更新閥值 return;//結束transfer } //如果沒完成,嘗試將sc -1. 表示這個執行緒結束幫助擴容了,將 sc 的低 16 位減一。 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { //如果 sc - 2 不等於識別符號左移 16 位。如果他們相等了,說明沒有執行緒在幫助他們擴容了。也就是說,擴容結束了。 /** *第一個擴容的執行緒,執行transfer方法之前(helpTransfer方法中),會設定 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2) *後續幫其擴容的執行緒,執行transfer方法之前,會設定 sizeCtl = sizeCtl+1 *每一個退出transfer的方法的執行緒,退出之前,會設定 sizeCtl = sizeCtl-1 *那麼最後一個執行緒退出時: *必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT */ if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) return;// 不相等,說明不到最後一個執行緒,直接退出transfer方法 finishing = advance = true;// 如果相等,擴容結束了,更新 finising 變數 i = n; // recheck before commit,最後退出的執行緒要重新check下是否全部遷移完畢 } } else if ((f = tabAt(tab, i)) == null) // 獲取老tab的i下標位置的變數,如果是 null,就使用 fwd 佔位。 advance = casTabAt(tab, i, null, fwd);// 如果成功寫入 fwd 佔位,再次推進一個下標 else if ((fh = f.hash) == MOVED)// 如果不是 null 且 hash 值是 MOVED。 advance = true; // already processed,說明別的執行緒已經處理過了,再次推進一個下標 else {// 到這裡,說明這個位置有實際值了,且不是佔位符。對這個節點上鎖。為什麼上鎖,防止 putVal 的時候向連結串列插入資料 synchronized (f) { // 判斷 i 下標處的桶節點是否和 f 相同 if (tabAt(tab, i) == f) { Node<K,V> ln, hn;// low, height 高位桶,低位桶 // 如果 f 的 hash 值大於 0 。TreeBin 的 hash 是 -2 if (fh >= 0) { // 對老長度進行與運算(第一個運算元的的第n位於第二個運算元的第n位如果都是1,那麼結果的第n為也為1,否則為0) // 由於 Map 的長度都是 2 的次方(000001000 這類的數字),那麼取於 length 只有 2 種結果,一種是 0,一種是1 // 如果是結果是0 ,Doug Lea 將其放在低位,反之放在高位,目的是將連結串列重新 hash,放到對應的位置上,讓新的取於演算法能夠擊中他。 int runBit = fh & n; Node<K,V> lastRun = f; // 尾節點,且和頭節點的 hash 值取於不相等 // 遍歷這個桶 for (Node<K,V> p = f.next; p != null; p = p.next) { // 取於桶中每個節點的 hash 值 int b = p.hash & n; // 如果節點的 hash 值和首節點的 hash 值取於結果不同 if (b != runBit) { runBit = b; // 更新 runBit,用於下面判斷 lastRun 該賦值給 ln 還是 hn。 lastRun = p; // 這個 lastRun 保證後面的節點與自己的取於值相同,避免後面沒有必要的迴圈 } } if (runBit == 0) {// 如果最後更新的 runBit 是 0 ,設定低位節點 ln = lastRun; hn = null; } else { hn = lastRun; // 如果最後更新的 runBit 是 1, 設定高位節點 ln = null; }// 再次迴圈,生成兩個連結串列,lastRun 作為停止條件,這樣就是避免無謂的迴圈(lastRun 後面都是相同的取於結果) for (Node<K,V> p = f; p != lastRun; p = p.next) { int ph = p.hash; K pk = p.key; V pv = p.val; // 如果與運算結果是 0,那麼就還在低位 if ((ph & n) == 0) // 如果是0 ,那麼建立低位節點 ln = new Node<K,V>(ph, pk, pv, ln); else // 1 則建立高位 hn = new Node<K,V>(ph, pk, pv, hn); } // 其實這裡類似 hashMap // 設定低位連結串列放在新陣列的 i setTabAt(nextTab, i, ln); // 設定高位連結串列,在原有長度上加 n setTabAt(nextTab, i + n, hn); // 將舊的連結串列設定成佔位符,表示處理過了 setTabAt(tab, i, fwd); // 繼續向後推進 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); // 和連結串列相同的判斷,與運算 == 0 的放在低位 if ((h & n) == 0) { if ((p.prev = loTail) == null) lo = p; else loTail.next = p; loTail = p; ++lc; } // 不是 0 的放在高位 else { if ((p.prev = hiTail) == null) hi = p; else hiTail.next = p; hiTail = p; ++hc; } } // 如果樹的節點數小於等於 6,那麼轉成連結串列,反之,建立一個新的樹 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; } } } } } }
-
-
addCount:計數
// 從 putVal 傳入的引數是x=1,check=binCount預設是0,只有hash衝突了才會大於1,且他的大小是連結串列的長度(如果不是紅黑樹結構的話,紅黑樹=2)。 private final void addCount(long x, int check) { CounterCell[] as; long b, s; //如果計數盒子不是空或者修改 baseCount 失敗 if ((as = counterCells) != null || !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; // 如果計數盒子是空(尚未出現併發) // 如果隨機取餘一個陣列位置為空 或者 // 修改這個槽位的變數失敗(出現併發了) // 執行 fullAddCount 方法,在fullAddCount自旋直到CAS操作成功才結束退出 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(); } // 檢查是否需要擴容,在 putVal 方法呼叫時,預設就是要檢查的(check預設是0,連結串列是連結串列長度,紅黑樹是2),如果是值覆蓋了,就忽略 if (check >= 0) { Node<K,V>[] tab, nt; int n, sc; // 如果map.size() 大於 sizeCtl(達到擴容閾值需要擴容) 且 // table 不是空;且 table 的長度小於 1 << 30。(可以擴容) while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { // 根據 length 得到一個標識 int rs = resizeStamp(n); if (sc < 0) {//表明此時有別的執行緒正在進行擴容 // 如果 sc 的低 16 位不等於 識別符號(校驗異常 sizeCtl 變化了) // 如果 sc == 識別符號 + 1 (擴容結束了,不再有執行緒進行擴容)(預設第一個執行緒設定 sc ==rs 左移 16 位 + 2,當第一個執行緒結束擴容了,就會將 sc 減一。這個時候,sc 就等於 rs + 1) // 如果 sc == 識別符號 + 65535(幫助執行緒數已經達到最大) // 如果 nextTable == null(結束擴容了) // 如果 transferIndex <= 0 (轉移狀態變化了) // 結束迴圈 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; // 不滿足前面5個條件時,嘗試參與此次擴容,把正在執行transfer任務的執行緒數加1,+2代表有1個,+1代表有0個,表示多了一個執行緒在幫助擴容,執行transfer if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } //如果不在擴容,將 sc 更新:識別符號左移 16 位 然後 + 2. 也就是變成一個負數。高 16 位是識別符號,低 16 位初始是 2. //試著讓自己成為第一個執行transfer任務的執行緒 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount();// 重新計數,判斷是否需要開啟下一輪擴容 } } }
-
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()); //table有值,且查詢到的槽位有值(tabAt方法通過valatile讀) if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { //hash、key、value都相同返回當前查詢到節點的值 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } //遍歷特殊節點:紅黑樹、已經遷移的節點(ForwardingNode)等 else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; //遍歷node連結串列(e.next也是valitle變數) while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; } 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; }
-
remove:
public V remove(Object key) { return replaceNode(key, null, null); } //通過volatile設定第i個節點的值 static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); } 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; //陣列或查詢的槽位為空,結束自旋返回null if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) break; //正在擴容,幫助擴容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null;//返回的舊值 boolean validated = false;//是否進行刪除連結串列或紅黑樹節點 synchronized (f) {//槽位加鎖 //getObjectVolatile獲取tab[i],如果此時tab[i]!=f,說明其他執行緒修改了tab[i]。回到for迴圈開始處,重新執行 if (tabAt(tab, i) == f) {//槽位節點沒有變化 if (fh >= 0) {//槽位節點是連結串列 validated = true; //遍歷連結串列 for (Node<K,V> e = f, pred = null;;) { K ek; //hash、key、value相同 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)//值覆蓋,replace()呼叫 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; //樹有節點且查詢的節點不為null 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)//值覆蓋,replace()呼叫 p.val = value; else if (t.removeTreeNode(p))//刪除節點成功 setTabAt(tab, i, untreeify(t.first));//更新當前槽位(陣列槽位)節點為樹的第一個節點 } } } } } if (validated) { //如果刪除了節點,更新size if (oldVal != null) { if (value == null) addCount(-1L, -1);//數量-1 return oldVal; } break; } } } return null; }
三.總結
-
put:使用cas插入,如果是連結串列或樹節點才會加鎖同步操作,提高了效能
- 不允許有key或value為null,否則丟擲異常;
- 在第一次put時初始化table(initTable()),初始化有併發控制,通過sizeCtl變數判斷,sizeCtl<0表示已經有執行緒在初始化,當前執行緒就不在進行,否則sizeCtl置為-1(CAS)並建立陣列;
- 當hash計算出的槽位節點為null時,使用CAS插入元素;
- 當hash為MOVED(-1)時,幫助擴容,但可能幫助不了,因為每個執行緒預設16個桶,如果只有16個桶,第二個執行緒無法幫助擴容;
- 如果hash衝突了,同步槽位節點,如果槽位是連結串列結構,進行連結串列操作,覆蓋舊值或插入到連結串列尾部;如果是樹結構,新增到樹中;
- 元素新增到連結串列或樹中,如果連結串列長度大於8,將連結串列轉換為紅黑樹;
- 呼叫addCount(),對size+1,並判斷是否需要擴容addCount(),如果是值覆蓋操作就不需要呼叫該方法;
-
initTable:初始化
- 陣列table為null才進行初始化,否則直接返回舊陣列;
- 如果當前sizeCtl小於0,表示有執行緒正在初始化,則當前執行緒禮讓CPU,保證只有一個執行緒正在初始化陣列;
- 如果沒有執行緒在初始化,則當前執行緒CAS將sizeCtl置為-1並建立陣列,然後重新計算閥值;
-
helpTransfer:幫助擴容
- 當嘗試插入操作時,發現節點是forward型別,則會幫助擴容;
- 每次加入一個執行緒都會將sizeCtl的低16位+1,同時校驗高16位的識別符號;
- 擴容最大的幫助執行緒是65535,這是低16位的最大值限制;
- 每個執行緒預設分配16個桶,如果桶的數量是16,那麼第二個執行緒無法幫助擴容,即桶被分配完其他執行緒無法進場擴容;
-
transfer:擴容和資料遷移
- 根據CPU核數平均分配給每個CPU相同數量的桶,如果不夠16個,預設就是16個;
- 按照2倍容量進行擴容;
- 每個執行緒在處理完自己領取的區間後,還可以繼續領取,如果還有的話,通過transferIndex變數遞減16實現桶數量控制;
- 每次處理空桶的時候,會把當前桶標識為forward節點,告訴put的其他執行緒說“我正在擴容,快來幫忙”,但如果只有16個桶,只能有一個執行緒進行擴容;
- 如果有了佔位符MOVED,表示已經被處理過,跳過這個桶,繼續推進處理其他桶;
- 如果有真正的實際值,那麼就同步加鎖頭節點,防止putVal的併發;
- 同步塊裡將連結串列拆分成兩份,根據 hash & length 得到是否是0,如果是0,放在新陣列低位,反之放在length+i的高位。這是防止下次取值hash找不到正確的位置;
- 如果該桶型別是紅黑樹,也會拆分成2個,然後判斷拆分過的桶的大小是否小於等於6,如果是轉換成連結串列;
- 執行緒處理完如果沒有可選區間,且任務沒有完成,則會將整個表檢查一遍,防止遺漏;
-
addCount:擴容判斷
- 當插入結束時,會對size+1,並判斷是否需要擴容的判斷;
- 優先使用計數盒子(如果不是空,說明併發了),如果計數盒子是空,使用baseCount變數+1;
- 如果修改baseCount失敗,使用計數盒子,如果還是修改失敗,在fullAddCount()中自旋直到CAS操作成功;
- 檢查是否需要擴容;
- 如果size大於等於sizeCtl且長度小於1<<30,可以擴容;
- 如果已經在擴容,幫助其擴容;
- 如果沒有在擴容,自行開啟擴容,更新sizeCtl變數為負數,賦值為識別符號高16位+2;
-
remove:刪除元素
- 自旋遍歷數量,如果陣列或根據hash計算的槽位節點值為null,直接結束自旋返回null;
- 如果槽位節點正在擴容,幫助擴容;
- 如果槽位節點有值,同步加鎖;
- 如果該槽位節點還是沒有任何變化,判斷是連結串列結構型別節點還是樹結構型別節點,通過遍歷查詢元素,找到刪除該節點或重新設定頭節點;
- 如果刪除了節點,更新size-1,如果有舊值則返回舊值,否則返回null;
四.參考
- https://www.jianshu.com/p/2829fe36a8dd
- https://www.jianshu.com/p/487d00afe6ca
- https://juejin.im/post/5b001639f265da0b8f62d0f8#comment