ConcurrentHashMap
內部結構
在JDK1.8之前的實現結構是:ReentrantLock+Segment+HashEntry+鏈表
JDK1.8之後的實現結構是:synchronized+CAS+Node+鏈表或紅黑樹(與HashMap一致)
而1.8之前鎖的是Segment,1.8鎖的是Node數組裏的Node,準確來說是頭結點。如圖虛線所示:
爲什麼要廢棄鎖分段機制:
1. 分段造成內存浪費(內存不連續,碎片化)
2. 在添加時競爭同一個鎖的概率非常小,分段鎖反而會造成更新等操作的長時間等待;並且當某個段很大時,分段鎖的性能會下降。
3. 爲了提高 GC 的效率
爲什麼加鎖不用ReentrantLock而是用synchronized:
1. 鎖的細化,之前ReentrantLock鎖住的是整個段,現在synchronized鎖住的是單個Node。
2. 因爲鎖的細化,出現競爭的情況大大減少。
3. 如果競爭同一個Node,只要線程可以在自旋有限次數內拿到鎖,Synchronized就不會升級爲重量級鎖,而等待的線程也就不用被掛起,我們也就少了掛起和喚醒這個上下文切換的過程開銷;而ReentrantLock不會自旋,只會掛起,多了個上下文切換的開銷。
爲什麼容量最好爲2的冪:
當數組長度爲2的n次冪的時候,不同的key算得hash相同的機率較小,那麼數據在數組上分佈就比較均勻,也就是發生碰撞的機率較小,進而導致鏈表結構減少,查詢的時候不用遍歷鏈表的話查詢效率就高了。
爲什麼get不用加鎖:
前面我畫的圖裏,Node的成員變量val是用volatile關鍵字修飾的,其它線程做出的修改能夠馬上看見,保證每次讀取的都是最新的數據。
源碼
put
final V putVal(K key, V value, boolean onlyIfAbsent) { // key和value 不能爲null if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; // 遍歷Node數組 for (Node<K,V>[] tab = table;;) { // f存儲當前位置數組上的Node,n代表數組長度,i代表當前數組下標,fh代表當前Node的hash值 Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) // 爲空或者長度爲0,初始化數組 tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 目標位置的值爲null,利用CAS設置value,返回。 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) // 如果hash值等於-1,代表正在擴容,helpTransfer會幫助擴容 tab = helpTransfer(tab, f); else { V oldVal = null; // 加鎖進入 synchronized (f) { // 再獲取一下當前位置的Node,如果和前面獲取的f不一致則發生了變化,跳出同步塊 if (tabAt(tab, i) == f) { // fh爲正數,代表鏈表結構 if (fh >= 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; // 如果已經遍歷到了最後(e.next==null),則直接插入到最後 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // fh < 0代表正在擴容或者紅黑樹結構 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; // key衝突,則覆蓋舊值 if (!onlyIfAbsent) p.val = value; } } } } // binCount爲當前位置包含的的Node數量,如果不是0,則判斷是否需要擴容 if (binCount != 0) { // Node數量大於等於8,當前位置的數據類型轉爲樹 if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); // 如果oldVal不爲空,證明存在覆蓋的情況,直接返回舊值 if (oldVal != null) return oldVal; break; } } } // 整個Map的Node數量+1,如果需要擴容則進行擴容 addCount(1L, binCount); return null; }
過程和HashMap類似:
0. Node數組沒有初始化先去初始化;
1. 根據hash找到數組中的位置,如果此位置爲空,則直接利用CAS將新節點放在此處;
2. 如果當前位置不爲空,則判斷此位置的Node的hash是否等於-1,等於-1代表正在進行擴容操作,調用helpTransfer協助擴容;
3. 此位置Node的hash不等於-1,則對其進行加鎖:
4. 如果此位置Node的hash大於等於0,證明這是個鏈表結構,先看是否存在相同的key,有則覆蓋,無則把新結點添加到鏈表最後;
5. 否則判斷當前節點是否是樹節點,如果是樹節點,則添加到樹中,有重複的key同樣會覆蓋;
6. 退出同步塊,判斷binCount(Node計數器)如果大於等於8,則把當前位置的鏈表轉變成紅黑樹;(這裏可以看出,binCount主要服務於鏈表結構,具體位置統計當前鏈表的大小)
7. 最後把整個Map的Node總數+1,如果需要擴容則擴容。
下面看一下initTable【初始化的過程】
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // table爲空並且sizeCtl < 0,有其它線程在初始化,則調用Thread.yield(),讓掉自己的CPU執行時間 if ((sc = sizeCtl) < 0) // 不擴容時:sizeCtl=數組長度*擴容因子;擴容和初始化table時:sizeCtl < 0 Thread.yield(); // 放棄初始化的競爭,僅僅自旋 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // sizeCtl > 0,說明沒有線程競爭初始化table,利用CAS將sizeCtl設置爲-1,代表正在初始化 try { // 再次判斷table是否爲空 if ((tab = table) == null || tab.length == 0) { // 設置table容量,如果sc大於0則使用sc,否則使用默認的16 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 根據容量new一個Node數組 @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 新數組替換老數組 table = tab = nt; // sc = 新容量 - (新容量/2^2),無符號右移2位,相當於除以2^2=4 // 以16爲例:sc = 16-(16/4)= 16-4 = 12,也就是下一次擴容的閾值 sc = n - (n >>> 2); } } finally { // 最後,更新sizeCtl sizeCtl = sc; } break; } } // 返回新數組 return tab; }
總結:
1. 根據sizeCtl判斷,如果小於0,表示正在初始化,則讓出當前線程的時間片。
2. 設置sizeCtl爲-1,代表正在執行初始化操作;如果sc存儲的變量大於0,則新容量=sc,否則等於默認容量16;根據新容量new一個新Node數組,並更新table爲新數組;更新sizeCtl爲新容量的75%
再來看一下addCount【Node總數+1 & 擴容的過程】
/** * sizeCtl(-1表示table正在初始化,其他線程要讓出CPU時間片;-N表示有N-1個線程正在執行擴容操作;大於0表示擴容閾值=容量*負載因子) * @param x 需要加上的數量 * @param check if <0, don't check resize, if <= 1 only check if uncontended */ private final void addCount(long x, int check) { // CounterCell:顧名思義,用於計數的格子。說白了就是用來統計table中每一個位置的Node數量。 CounterCell[] as; long b, s; // CounterCell不爲null if ((as = counterCells) != null || // 或者利用CAS將baseCount更新爲baseCount+1失敗,就放棄對baseCount的操作 !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { CounterCell a; long v; int m; boolean uncontended = true; 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; // 合計Node總數,其中的實現是遍歷CounterCell[],累加其中的value s = sumCount(); } // check>=0,需要檢查是否需要擴容 if (check >= 0) { // tab:指向table,nt:指向nextTable;n爲當前table的容量,sc爲當前擴容閾值 Node<K,V>[] tab, nt; int n, sc; // Node總數大於擴容閾值sizeCtl 並且 table不爲空 並且 table容量小於最大容量 while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) { int rs = resizeStamp(n); // 如果正在擴容 if (sc < 0) { // 如果sizeCtl變化了或者擴容結束了,則跳出循環 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; // 如果可以幫助擴容,那麼將 sc 加 1. 表示多了一個線程在幫助擴容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } // 如果沒有擴容,將 sc 更新爲負數,表示當前線程發起擴容操作 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } } }
總結:
1. 使table的長度+1。CounterCell不爲null,就使用CounterCell,否則直接利用CAS操縱baseCount。
2. 如果需要擴容,先看是否已經在擴容了,如果是,則加入擴容線程,否則就調用擴容方法開啓擴容。
最後看transfer方法,這是擴容過程 的核心
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { // n爲當前數組大小,stride存儲步長 int n = tab.length, stride; // 根據cpu核數計算出步長,用於分割擴容任務,方便其餘線程幫助擴容,最小爲16 // 默認每個線程處理16個桶。因此,如果長度是16的時候,擴容的時候只會有一個線程擴容。 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE; // subdivide range // 判斷nextTab是否爲空,nextTab是暫時存儲擴容後的node的數組,第一次進入這個方法的線程纔會發現nextTab爲空 if (nextTab == null) { // initiating try { // 初始化nextTab,容量是tab的2倍 @SuppressWarnings("unchecked") 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; // 當前數組長度賦給transferIndex transferIndex = n; } // nextTab的大小 int nextn = nextTab.length; ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); boolean advance = true; // finishing爲true代表擴容結束 boolean finishing = false; // to ensure sweep before committing nextTab for (int i = 0, bound = 0;;) { Node<K,V> f; int fh; // 進入一個 while 循環,分配數組中一個桶的區間給線程. 從大到小進行分配。當拿到分配值後,進行 i-- 遞減。這個 i 就是數組下標。 while (advance) { int nextIndex, nextBound; if (--i >= bound || finishing) advance = false; else if ((nextIndex = transferIndex) <= 0) { i = -1; advance = false; } else if (U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) { bound = nextBound; i = nextIndex - 1; advance = false; } } if (i < 0 || i >= n || i + n >= nextn) { int sc; // 如果擴容結束 if (finishing) { // 清除臨時變量 nextTable = null; // 更新table變量 table = nextTab; // 更新sizeCtl,這個等價於新容量*0.75 sizeCtl = (n << 1) - (n >>> 1); return; } // 嘗試將 sc -1. 表示這個線程結束幫助擴容了 if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) { // 果 sc - 2 不等於標識符左移 16 位。如果他們相等了,說明沒有線程在幫助他們擴容了。也就是說,擴容結束了。 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT) // 不相等,說明沒結束,當前線程結束方法。 return; // 如果相等,擴容結束了,更新 finising 變量 finishing = advance = true; // 再次循環檢查一下整張表 i = n; // recheck before commit } } // 如果沒有完成任務,且 i 對應的槽位是空,嘗試 CAS 插入佔位符,讓 putVal 方法的線程感知。 else if ((f = tabAt(tab, i)) == null) advance = casTabAt(tab, i, null, fwd); // 如果 i 對應的槽位不是空,且有了佔位符,那麼該線程跳過這個槽位,處理下一個槽位。 else if ((fh = f.hash) == MOVED) advance = true; // already processed else { // 如果以上都是不是,說明這個槽位有一個實際的值。開始同步處理這個桶。 // 到這裏,都還沒有對桶內數據進行轉移,只是計算了下標和處理區間,然後一些完成狀態判斷。同時,如果對應下標內沒有數據或已經被佔位了,就跳過了。 // 下面的處理過程和HashMap基本一樣 synchronized (f) { // 再次判斷當前節點是否發生了改變 if (tabAt(tab, i) == f) { // ln=lowNode=低位桶,hn=highNode=高位桶 Node<K,V> ln, hn; // 當前是鏈表結構 if (fh >= 0) { // 當前節點hash和老長度進行與運算 int runBit = fh & n; Node<K,V> lastRun = f; // 從當前節點的後繼開始遍歷 for (Node<K,V> p = f.next; p != null; p = p.next) { // 對每個節點的hash同長度進行按位與操作 int b = p.hash & n; // 如果節點的 hash 值和首節點的 hash 值按位與結果不同 if (b != runBit) { // 更新 runBit,用於下面判斷 lastRun 該賦值給 ln 還是 hn。 runBit = b; // 這個 lastRun 保證後面的節點與自己的按位與值相同,避免後面沒有必要的循環 lastRun = p; } } if (runBit == 0) { // 如果最後更新的 runBit 是 0 ,設置低位節點 ln = lastRun; hn = null; } else { // 否則設置高位節點 hn = lastRun; ln = null; } // 從頭開始循環,目的是生成兩個鏈表,lastRun 作爲停止條件,這樣做爲了避免不必要的循環(lastRun 後面都是相同的hash按位與結果) 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) 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); // 在老數組i的位置的鏈表設置成佔位符 setTabAt(tab, i, fwd); // 繼續向後 advance = true; } // 樹結構 else if (f instanceof TreeBin) { // 當前位置的頭節點,只不過是TreeNode 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); // 當前節點的hash和老長度做按位與操作,爲0放在低位 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; // 在新的數組i的位置上設置低位樹 setTabAt(nextTab, i, ln); // 在新的數組i+n的位置上設置高位鏈表 setTabAt(nextTab, i + n, hn); // 老數組i的位置上設置佔位符 setTabAt(tab, i, fwd); // 繼續向後 advance = true; } } } } } }
總體來說分爲兩部分:
1. 擴容前的準備和相關狀態的檢查
①:初始化用於存儲擴容後數據的nextTable
②:分配一個桶給當前線程;判斷是否擴容結束,擴容結束更新table和sizeCtl變量;判斷當前桶是不是被佔用了,被佔用則跳過這個桶;
2. 加鎖擴容
①:判斷節點類型
②:如果是鏈表,從頭結點開始遍歷鏈表,通過當前節點和老長度的按位與操作生成一個runBit,每次遇到與前一個runBit不同的節點,則更新runBit和lastRun(當前與前面runBit不同的節點),直到遍歷結束;
③:根據runBit是否爲0,把lastRun節點賦給低位鏈表或者高位鏈表;
④:再次遍歷鏈表,分割出兩部分鏈表:以lastRun節點爲停止遍歷條件,根據每個Node的hash和老長度的按位與結果是否爲0,把Node劃分到低位鏈表和高位鏈表中。最後把低位鏈表和高位鏈表放到新數組i和i+n的位置上,老數組i的位置上設置佔位符。繼續處理其它剩餘的桶。
⑤:處理樹形結構,邏輯和鏈表一樣,只不過多了個判斷是否退化成鏈表的邏輯。
擴容過程我畫了個圖
最後設置新位置
未完待續
ConcurrentSkipListMap
--
TreeMap
--
.