ConcurrentHashMap 的 Traverser 閱讀

  ConcurrentHashMap 源碼目前在網絡上已有衆多解析。本文章主要關注其基於 Traverser 的遍歷實現,試圖仔細解析該實現,若有錯漏,請指正。
  ConcurrentHashMap 的 Traverser 主要是用於內部數組的遍歷功能支持,如何實如今內部數組擴容階段期間,其餘線程也可以正確地遍歷輸出,並保證良好的性能(不使用各類鎖),Traverser 提供了一個優秀的設計實現。

  1. 相關概念
  2. 解析node

  1.相關概念     

    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.解析

    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 其餘問題:

      • 當遍歷到 ForwardingNode ,將信息臨時存儲起來後,若是此時 ConcurrentHashMap 的擴容結束,會發生什麼狀況?
        • 實際上,Traverser 的遍歷是與外部的擴容關聯不大(處理新表),由於 Traverser 在建立的時候,已經使用了內部變量存儲了 舊錶 的引用,因此即便外部的擴容結束,原 ConcurrentHashMap 的表引用被更新爲新表,但 Traverser 的舊錶引用還在,還可以一致基於舊錶進行遍歷,但這個時候,由於舊錶上已經所有是 ForwardingNode 了,因此會不斷地經過 ForwardingNode 找到新表,並進行 TableStack 的存儲和恢復,每一次元素遍歷都在 baseIndex 和 baseIndex + baseSize 索引上獲取節點數據。
      • 當遍歷過程當中,有新元素添加,若是添加到 index 索引以前,會發生什麼狀況?
        • 什麼都不會發生,在 Traverser 的算法中,認爲這即便發生了,也屬於 可保證的一致性,對遍歷沒有影響。固然若是是被添加到 index 以後,仍是可以被遍歷到的。

    

    總結:

  • 若是可以在腦海中,將遍歷想象成兩個表,一箇舊表,一個新表(新表大小爲舊錶的 2 倍),索引移動遇到元素標識爲 rehashed 時,就切換到新表作(2處不一樣索引的)元素獲取,那這個 Traverser 就不難理解。
  • 在遍歷中爲了保存臨時信息,同時爲了儘量地複用,Traverser 實現了 TableStack 的結構,雖然看起來有點繞,但複用槓槓的    
  • Traverser 的遍歷實現也得益於 ConcurrentHashMap 的擴容是 2倍擴容,便於推測新數組的邊界範圍
  • 簡略圖以下:

    

相關文章
相關標籤/搜索