ConcurrentHashMap 源碼目前在網絡上已有衆多解析。本文章主要關注其基於 Traverser 的遍歷實現,試圖仔細解析該實現,若有錯漏,請指正。
ConcurrentHashMap 的 Traverser 主要是用於內部數組的遍歷功能支持,如何實如今內部數組擴容階段期間,其餘線程也可以正確地遍歷輸出,並保證良好的性能(不使用各類鎖),Traverser 提供了一個優秀的設計實現。
1. 相關概念
2. 解析node
1.1 ConcurrentHashMap 支持方法 forEach(Consumer<? super K> action)、keys、elements、contains 等方法,它們的內部實現都基於 Traverser;算法
(基於 Traverser 的方法)數組
1.2 ConcurrentHashMap 的遍歷可以保證在內部數組擴容期間,也具備優秀的性能和併發安全性。安全
1.3 Traverser 的主要方法:advance,其表示爲:推動,與擴容期間時的 boolean 變量 advance 具備類似的意義,都是推動索引(index)的增加,以便持續向前推動元素定位。網絡
1.4 理解在 類 Traverser 中的變量的命名,有利於對實現的理解,如:baseXxx 的變量是針對 ConcurrentHashMap 中的舊錶,沒有 base- 前綴的則同時針對舊錶與新表(當前遍歷上下文)。併發
2.1 ConcurrentHashMap 統一了遍歷的基礎實現,即:Traverser 負責元素推動,並將元素返回;而 BaseIterator 則負責對元素進行判斷,並提供迭代實現:hasNext、next;XxxIterator 則是基於 BaseIterator 提供不一樣的實現:針對keys的迭代、針對value的迭代、針對 entry 的迭代。性能
2.2 Traverser 類保持了迭代的索引:index,和對所在 ConcurrentHashMap 的內部數組—— 多是舊錶,也多是新表的引用this
static class Traverser<K,V> { // 默認是當前 table,若是正在重組,則會被間斷性替換爲 nextTable Node<K,V>[] tab; // current table; updated if resized // next 是一個關鍵的基礎變量,做爲迭代中的當前元素,主要實現:next() 和 hasNext() Node<K,V> next; // the next entry to use // 爲了達到複用效果,使用一個 TableStack 在 這兩個變量之間相互替換 TableStack<K,V> stack, spare; // to save/restore on ForwardingNodes 保存或恢復 // 真正遍歷時的索引,有可能反覆橫跳,默認使用中爲 index,而在 resize 中,則多是 index、baseIndex+baseSize(由於翻倍) int index; // index of bin to use next /** * base* 是基於舊 tab(若是擴容,則存在 新 tab) * baseIndex 是在舊 tab 上的偏移 * baseSize 是舊 tab 的大小,這是爲了方便存在 newTable 時,用於計算,由於 newTable.length = 2*baseSize * baseLimit 是要遍歷的區間範圍(也就是確定是 limit <= size) */ int baseIndex; // current index of initial table int baseLimit; // index bound for initial table final int baseSize; // initial table size Traverser(Node<K,V>[] tab, int size, int index, int limit) { this.tab = tab; this.baseSize = size; this.baseIndex = this.index = index; this.baseLimit = limit; this.next = null; }
// ...
}
在 Traverser 的構造方法中,目前的引用,都是 limit = size,也就是說,目前被引用的地方,都是對整個內部數組進行迭代,區間控制在 0 ~ limit = size 。spa
2.3 Traverser 的暴露的方法(修飾符爲 default),只有:advance。線程
advance 負責推動元素,在擴容階段,也可以讓索引到新表中進行元素遍歷。
final Node<K,V> advance() { Node<K,V> e; // 這是推動成功(索引增長)讓外部獲取到元素後,外部從新進入的流程 // 嘗試賦值爲 e.next,若是不爲null,則進行鏈表的迭代或樹的遍歷,不推動到下一節點 if ((e = next) != null) e = e.next; for (;;) { // t -> table,要遍歷的 table // i -> index ; n -> length/number Node<K,V>[] t; int i, n; // must use locals in checks // 已經在上一步賦值爲 e.next,進行迭代,若是爲 null,才推動下一節點 if (e != null) return next = e; /** * baseIndex >= baseLimit 最開始傳入的 index 就超過 limit,顯然是不正確的,直接返回 null * 邊界判斷,這裏的 tab 有可能便是 old table 也有多是 nextTable, * 這也致使 n 的值反覆橫跳,有時是 old table 的長度,有時是 nextTable 的長度 * 但 t.length(新舊錶的大小)總會比當前正在遍歷的偏移 index 大。 * 而且下面的判斷:(index = i + baseSize) >= n 是有可能成立的 */ if (baseIndex >= baseLimit || (t = tab) == null || (n = t.length) <= (i = index) || i < 0) return next = null; /** * 若是節點的 哈希 < 0,有多種狀況: * 一是判斷是否正在重組(在重組中被轉移了) * 若是是 ForwardingNode(hash=-1) 說明該舊錶節點已經在重組中被轉移了, * 使用的算法是:對於已經移動的節點(ForwardingNode),換到新表(nextTable),index 保持不變, * continue 跳出,準備到新表進行迭代 * 存儲信息到 臨時stack 中 * 將該節點的索引推入到棧中,並更新遍歷的表格爲 nextTable,從新進入(index 不變), * 則 tabAt(t,i) * 下輪遍歷開始,tabAt 已是針對 nextTable,此時再也不多是 ForwardingNode,在 nextTable 遍歷完後 * 回來經過 recoverState 將 tab 從新替換爲 舊table,並推動索引 * 二是:標識該節點是樹節點,是則獲取樹結構的 first 節點,並向下執行,在 recoverState 中,將 index = index+baseSize * 也就是,將 index 跨幅度增長爲元素索引在新表中 rehash 後的位置,以便下次循環從該位置開始 * 總結:也就是說,在舊錶遇到 ForwardingNode 後,遍歷將切換到新表, * 對索引 index 和 baseIndex + baseSize 上的元素進行遍歷 */ if ((e = tabAt(t, i)) != null && e.hash < 0) { if (e instanceof ForwardingNode) { tab = ((ForwardingNode<K,V>)e).nextTable; // 賦值爲 null,並 continue,以便去進行 recoverState 操做 e = null; // 保存臨時信息(當前索引到的位置、舊錶大小、舊錶引用) pushState(t, i, n); // 假設在這裏,擴容階段被完成了,將出現什麼狀況? continue; } // 樹節點類型的 hash 也爲負數:-2 else if (e instanceof TreeBin) e = ((TreeBin<K,V>)e).first; else e = null; } /** * 不管如何,程序老是會跑到此判斷上。 * 若是 stack 不爲 null,說明索引 i 已經被擴容進行過 rehash, * 那麼就須要遍歷完新表上的索引: baseIndex 和 baseIndex+baseSize, * 再從新賦值回舊錶,從 ++baseIndex 開始 */ if (stack != null) // 輔助跳到新表的 2倍 index 位置 recoverState(n); else if ((index = i + baseSize) >= n) // 超過 n(tab 大小,恢復爲基於舊錶偏移的大小並加 1) // 若是出現此狀況,訪問回舊槽 index = ++baseIndex; // visit upper slots if present } }
簡化描述迭代的代碼:
1. 遍歷器進行普通的遍歷,經過索引的增加(+1),來獲取節點,節點可能爲 鏈表結構、樹節點、null。若是爲鏈表節點,則須要如今該索引上進行:e.next 獲取節點的下一節點直到爲 null,才推動 index;若是爲 樹節點,則須要獲取 e.first,而後再經過 e.next 獲取下一個樹節點,直到爲 null;若是爲 null,則直接推動 index。
2. 遍歷過程當中,若是節點爲 ForwardingNode(不是以上三種類型),則將當前遍歷的數據(或稱舊錶)替換爲新數組(新表),並將當前遍歷的信息保存起來,主要有:當前在舊錶上遍歷的索引、舊錶、舊錶大小。
3. 基於新表,先在新表上對索引 baseIndex 進行第一步的節點獲取,而後經過 recoverState 對索引進行推動,推動算法爲:index = baseIndex + baseSize。由於在 ConcurrentHashMap 中,擴容的新數組大小爲:2 * 舊錶大小。
4. 基於新表,對 baseIndex + baseSize 上的元素獲取使用完成後,推動索引,此時算法中將比較,這次推動的大小是否超過新表大小,如是,則將臨時信息恢復,從新回到舊錶上進行遍歷。推動算法爲:
/** * n 老是爲新表大小,即:2 * baseSize * Possibly pops traversal state. * * @param n length of current table */ private void recoverState(int n) { // ...// s == null 做爲先決條件,表示退回舊錶進行操做 if (s == null && (index += baseSize) >= n) index = ++baseIndex; } }
5. 最終,是經過邊界判斷讓遍歷器退出。
2.4 advance 完成了在新舊錶間切換遍歷的實現,主要是藉助於兩個方法:pushState 、recoverState。其中,pushState 主要是負責存儲臨時信息,而 recoverState 則負責:當表處於新表時,輔組推動索引跨幅度增加,同時還負責切換爲舊錶時,將索引回退爲舊錶的索引位置並 +1.
/** * <pre> * 爲了達到複用的效果, * spare 老是被清理,稱爲 null,在 recoverState 中將被賦值一個清空的 stack * stack 老是被負責,以便在 recoverState 被使用,而後進行清空,等下次 pushState 被從新賦值 spare * </pre> * Saves traversal state upon encountering a forwarding node. */ private void pushState(Node<K,V>[] t, int i, int n) { // spare 備用 TableStack<K,V> s = spare; // reuse if possible if (s != null) spare = s.next; else s = new TableStack<K,V>(); // 存儲舊錶 s.tab = t; // 存儲舊錶長度 s.length = n; // 存儲遍歷的舊錶的索引 s.index = i; s.next = stack; // stack 存儲了節點 stack = s; } /** * n 老是爲新表大小,即:2 * baseSize * Possibly pops traversal state. * * @param n length of current table */ private void recoverState(int n) { TableStack<K,V> s; int len; // 此方法的第一次是爲 index 提供跨幅度增長,調整 index = baseIndex + baseSize // 此時的 index 老是 < n 的,因此循環不成立,退出到外部方法進行元素獲取 // 第二次,此時 index + baseSize = 2*baseSize + baseIndex > n, // 此狀況下,新表對應索引上的元素已經被獲取了,沒有其餘元素,則恢復臨時信息 while ((s = stack) != null && (index += (len = s.length)) >= n) { n = len; index = s.index; // 將舊錶賦值回遍歷器,而後將存儲值置爲 null tab = s.tab; s.tab = null; TableStack<K,V> next = s.next; // null s.next = spare; // save for reuse stack = next; spare = s; } // 此處恢復 index 爲 baseIndex + 1 // 將 index 調整爲舊錶 index,並自增推動到下一個元素 // s == null 做爲先決條件,表示退回舊錶進行操做 if (s == null && (index += baseSize) >= n) index = ++baseIndex; } }
pushState 和 recoverState 除了實現索引的變換,實際上對 TableStack 的操做,也達到經過複用單獨一個 TableStack 實現入棧出棧,重複使用的效果。主要是經過 TableStack 的 next 屬性。由於在算法中,經過判斷 TableStack 是否爲 null 來做爲先決條件控制將索引恢復爲舊錶索引+1,因此須要將實例化好的惟一一個 TableStack 不斷關聯到 next 屬性上,便於下次使用時,從 next 上獲取該對象。賦值 TableStack 爲 null 是在 recoverState 中處理的。經過兩個指針不斷循環關聯到 next 屬性上達到了複用的效果。
( 索引的推動 ) ( TableStack 的複用 )
2.5 其餘問題:
總結: