深刻分析ConcurrentHashMap

深刻分析ConcurrentHashMap

[TOC]java

簡介

在從JDK8開始,爲了提升併發度,ConcurrentHashMap的源碼進行了很大的調整。在JDK7中,採用的是分段鎖的思路。簡單的說,就是ConcurrentHashMap是由多個HashMap構成。當須要進行寫入操做的時候,會尋找到對應的HashMap,使用synchronized對對應的hashmap加鎖,而後執行寫入操做。顯然,併發程度就取決於HashMap個數的多少。而在JDK8中換了一種徹底不一樣的思路。node

首先,仍然是使用Entry[]做爲數據的基本存儲。可是鎖的粒度被縮小到了數組中的每個槽位上,數據讀取的可見性依靠volatile來保證。而在嘗試寫入的時候,會將對應的槽位上的元素做爲加鎖對象,使用synchronized進行加鎖,來保證併發寫入的安全性。算法

除此以外,若是多個Key的hashcode在取模後落在了相同的槽位上,在必定數量內(默認是8),採用鏈表的方式鏈接節點;超過以後,爲了提升查詢效率,會將槽位上的節點轉爲使用紅黑樹結構進行存儲。數組

還有一個比較大的改變在於當進行擴容的時候,除了擴容線程自己,若是其餘線程識別到了擴容進行中,則會嘗試協助擴容。安全

下面來看下來針對幾個重點方法進行源碼分析。數據結構

放入數據

添加數據的方法爲java.util.concurrent.ConcurrentHashMap#put,該內容實現委託給方法java.util.concurrent.ConcurrentHashMap#putVal多線程

該方法總體上能夠爲分爲三個部分:併發

  • 使用spread方法獲得key的hashcode
  • 將KV對在Entry[]尋找合適的位置放入
  • 容器內元素總數+1,而且在須要時執行擴容。

第一步沒什麼好說的,直接來看第二步的相關代碼,以下dom

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();//初始化數組,標記1
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;   //標記2             
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f); //標記3
            else {
                V oldVal = null;
                synchronized (f) {
                    //省略相關代碼,標記4
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i); //標記5
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }

代碼比較複雜,咱們分紅了5個標記進行說明。ide

首先是標記1,若是嘗試添加元素時發現table屬性爲null,則意味着整個容器還沒有初始化,此時執行初始化方法,也就是initTable,代碼以下

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

總體思路很明確,經過CAS爭奪sizeCtl屬性的控制權,成功將該值設置爲-1的線程能夠執行初始化工做,而其餘線程經過Thread.yield()進行等待,直到確認容器初始化完畢,也就是table屬性有了值。當初始化完畢時,sizeCtl會被設置爲下一次擴容的容量閥值,該值爲當前容量的3/4。

若是容器已經初始化,而且Key的hashcode對應的槽位爲空,則能夠考慮新建一個節點放入該槽位。也就是標記2。這裏解釋下槽位上數據的讀取,都是經過方法tabAt,代碼以下

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

該取值方法是經過計算對應槽位在數組中的便宜量的值,即((long)i << ASHIFT) + ABASE,也就是基礎偏移量+元素間隔偏移量。而且讀取的時候使用的是getObjectVolatile,該方法的讀取和對屬性使用volatile是同樣的效果,能夠保證讀取到最新的值。

接着來看標記2,在槽位爲null的狀況下,其對值的寫入採用了CAS方式,也是爲了保證併發的安全性。若是CAS成功,則元素添加完畢,能夠直接退出循環。若是CAS失敗,則意味着有其餘線程已經對相同的槽位操做成功,此時就要從新循環,確認最新的狀況。

若是對應的槽位不爲空,且其hashcode標識爲特定負數,也就是標識容器正在擴容的負數,此時須要協助進行容器擴容,也就是標記3

這裏對Key的hashcode作一個說明,因爲key的hashcode會通過方法spread處理,所以必然爲正數。而負數的hashcode有三個特殊的含義,分別是:

  • -1:表明容器在擴容,而且當前節點的數據已經前移到擴容後的數組中。
  • -2:表明當前槽位上的節點採用紅黑樹結構存儲。
  • -3:表明該節點正在進行函數式運算,節點值還未最終肯定。

協助擴容的分析與容器擴容放在一塊兒,這邊先暫時略過。

若是對應槽位不爲空,且hashcode不爲負數,就意味着該槽位能夠執行元素添加,也就是標記4。來看下對應的代碼,以下

synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            //省略相關代碼,其內容爲在鏈表上添加元素,將元素添加到隊列的末尾
                        }
                        else if (f instanceof TreeBin) {
                           //省略相關代碼,其內容爲在紅黑樹結構上添加元素
                        }
                    }
                }

爲了保證對同一個槽位上併發更新的安全性,須要對槽位上的節點執行加鎖操做。

取得鎖以後,首先確認當前槽位上的節點是否仍然是加鎖成功的節點,一致的狀況說明加鎖成功的先後,槽位上數據形式沒有變更,才能執行後續的操做。

加鎖完畢後,判斷槽位上節點的類型,若是hashcode大於等於0,是爲普通節點,意味着該槽位上的數據採樣鏈表形式存儲,不然判斷節點類型(必然爲紅黑樹節點,也就是TreeBin),確認其爲紅黑樹節點。

普通節點的添加很簡單,經過對比節點中的key和Value是否和要添加的KV對一致來判斷是否重複,沒有重複的狀況下就添加到隊尾。重複的狀況下則依據方法入參onlyIfAbsent的值判斷是否要進行替換。

紅黑樹節點的添加則比較複雜,具體算法能夠參看紅黑樹,這邊再也不贅述。

當元素添加成功後,若是當前槽位採用鏈表存儲節點,而且鏈表長度超過閥值,則將鏈表轉化爲紅黑樹結構。也就是標記5

數據放入完畢後,就是對容器內元素個數的總數進行增長操做了,也就是第三步的內容。

容器元素總數更新

元素總數更新是依靠方法addCount完成。該方法整體分爲兩個步驟:

  • 總數更新
  • 根據入參和當前總數,判斷是否執行擴容。

首先來看總數更新的部分,代碼以下

if ((as = counterCells) != null ||
            !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;
            s = sumCount();
        }

總體的更新思路實際上和JDK8新增的一個統計類是徹底一致的,即java.util.concurrent.atomic.LongAdder。這個類用於在更高的併發競爭下,下降或維持數字計算的延遲。其性能相較傳統的AtomicLong要更好。具體的代碼分析這邊就不展開了,可是說下核心思路:

  • 整個統計的數據結構包含一個基本的長整形變量baseCount和一個統計單元CounterCell構成的數組,數組的長度爲2的次方冪,初始長度爲2,最大長度超過CPU內核數時中止擴容。
  • 當統計數字須要變化時,優先在baseCount上執行CAS操做。若是CAS成功,則意味着更新完成。若是失敗,說明此時有多線程競爭,放棄在baseCount上的爭奪。
  • 當放棄在baseCount上的爭奪時,經過線程上的隨機數h在CounterCell[]數組上找到槽位,在槽位上的CounterCell內部的整型變量上循環執行CAS更新,直到成功。
  • 若是須要初始化CounterCell[]數組或者添加元素到具體槽位,或者庫容,只能一個線程進行,該線程須要對cellBusy這個屬性進行CAS爭奪而且成功。

這個算法的核心思路就是避免多線程在一個變量上循環CAS直到成功。由於當多線程競爭較爲激烈時,大量的線程會在不斷的CAS失敗中浪費不少CPU時間。經過線程變量的方法,將多線程分散到不一樣的CounterCell單元中,下降了競爭的烈度和顆粒度,所以可以提升併發效率。

因爲統計數據被分散在baseCountCounterCell[]中,執行總數計算時也須要遍歷這裏面全部的值相加才能獲得最終值。

總數更新完畢後,就到了擴容判斷環節了。

容器擴容

容器擴容判斷是在總數更新中的部分代碼實現的,具體以下

Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);//標記1
                if (sc < 0) {//標記2
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))//標記3
                    transfer(tab, null);
                s = sumCount();
            }

能夠看到,擴容的依據是sizeCtl這個屬性,當容器元素總數超過sizeCtl時,執行擴容流程。

首先第一步標記1,是對容器內當前數組長度計算蓋戳標記值,也就是resizeStamp,其具體代碼以下

static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }

因爲n是2的次方冪,Integer.numberOfLeadingZeros(n)是得到32位整型數字中,在第一個1的位以前有多少個0的結果,所以這個值實際上就是數字n的一種換算關係。

RESIZE_STAMP_BITS則意味着該結果可以佔據的比特位數。因爲Integer.numberOfLeadingZeros(n)最大值爲28(n的最小值爲16),所以RESIZE_STAMP_BITS最小也必須爲6。

這個方法計算出來的結果,實際上能夠當作是數組的長度的固定換算值。這個值能夠在多線程擴容過程用於判斷是否擴容完畢了。

這裏要對sizeCtl這個屬性作一下說明,其取值有以下規律:

  • 0:這是一個初始值,意味着此時數組還沒有初始化。
  • -1:這是一個控制值,意味着有線程取得了數組的初始化權利,而且正在執行初始化中。
  • 正數:該值是容器要擴容的閥值,一旦元素總數到達該值,則應該進行擴容。除非數組長度到達上限。
  • 非-1的負數:該值意味着當前數組正在擴容,該值的左邊RESIZE_STAMP_BITS個數的比特位用於存儲數組長度n的蓋戳標記,右邊32-RESIZE_STAMP_BITS位用於存儲當前參與擴容的線程數。

回到擴容的代碼,標記1代碼完成後,就開始判斷是執行擴容仍是協助擴容。若是sizeCtl當前值爲負數,就協助擴容也就是標記2;若是爲正數,就發起擴容,也就是標記3

首先來看標記3,也就是發起擴容。須要經過CAS對sizeCtl的值進行置換。發起擴容時須要置換的值的含義上面也說過,左邊是蓋戳標記,右邊是參與擴容的線程數。

來看下擴容的具體代碼,也就是transfer方法,該方法較爲複雜,具體區分爲幾個步驟:

  • 步驟一:計算當前線程本次前移的槽位個數
  • 步驟二:初始化擴容後的數組對象,賦值給屬性nextTable
  • 步驟三:按照步驟一計算的結果,從數組的末尾開始,每批遷移必定槽位上的節點到新的數組直到所有遷移完畢;將新的數組的值賦值給屬性table,將屬性nextTable設置爲null,計算新的sizeCtl,遷移完成。

首先來看步驟一,很簡單,只有一句代碼

if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE;

默認狀況下,每次遷移1/8的槽位。

步驟二同樣也很簡單,就是一個基本的賦值動做,就不展開了。

步驟三比較複雜,在細分爲幾個階段:

  • 階段一:計算本次遷移開始的槽位下標和數量。
  • 階段二:判斷遷移是否完成,若是完成則設置相關屬性。
  • 階段三:按照階段一的槽位下標和數量,執行遷移。

先來看階段一,代碼以下

boolean advance = true;
boolean finishing = false;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        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;
         }
         }
         //階段二代碼
         //階段三代碼
    }

transferIndex的初值爲數組的長度。肯定本次前移的槽位範圍是第二個else if來決定的。經過CAS爭奪,將transferIndex的值下降。CAS成功後,本次減小的transferIndex值對應的區域,就是本次遷移的區域。經過這種方式,每一個線程均可以在本身獨立的槽位範圍內做業而不會互相爭奪,避免競爭。

階段二用於判斷遷移是否完成,具體代碼以下

if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n;
                }
            }

i小於0時意味着遷移已經結束了,此時先減小遷移線程技術,也就是CAS代碼U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)完成的功能。經過確認是不是最後一個退出遷移的線程,也就是代碼 if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)完成的功能,來執行最後一次的檢查,也就是將i設置爲數組長度值n。再執行一次整體循環,檢查每個槽位都遷移完畢。

最後一次確認完畢後,就開始進行退出操做。也就是相關的賦值動做,這部分簡單,不展開說明了。

階段三用於執行遷移槽位,最爲複雜,來看代碼

else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true;
            else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            //省略代碼,從鏈表中遷移數據到新數組
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            //省略代碼,從紅黑樹中讀取元素放入新數組
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }

逐個槽位進行判斷,這個是經過外層最大的for循環來執行的。針對每個槽位,具體狀況具體分析。

  • 若是槽位爲null,則嘗試經過CAS將一個標識遷移的特殊節點,ForwardingNode放入槽位。
  • 若是槽位上的節點已是ForwardingNode,則忽略,尋找下一個槽位。
  • 不是以上兩種狀況,則對槽位節點加鎖。成功後,執行數據遷移,遷移完畢後,將槽位節點設置爲ForwardingNode,用以標識遷移完畢。

以鏈表的數據遷移爲例進行分析,代碼以下

int                          runBit  = fh & n;
        ConcurrentHashMap.Node<K, V> lastRun = f;
        for (ConcurrentHashMap.Node<K, V> p = f.next; p != null; p = p.next)
        {
            int b = p.hash & n;
            if (b != runBit)
            {
                runBit = b;
                lastRun = p;
            }
        }
        if (runBit == 0)
        {
            ln = lastRun;
            hn = null;
        }
        else
        {
            hn = lastRun;
            ln = null;
        }
        for (ConcurrentHashMap.Node<K, V> p = f; p != lastRun; p = p.next)
        {
            int ph = p.hash;
            K   pk = p.key;
            V   pv = p.val;
            if ((ph & n) == 0)
            {
                ln = new ConcurrentHashMap.Node<K, V>(ph, pk, pv, ln);
            }
            else
            {
                hn = new ConcurrentHashMap.Node<K, V>(ph, pk, pv, hn);
            }
        }
        setTabAt(nextTab, i, ln);
        setTabAt(nextTab, i + n, hn);

對於數組長度爲n,下標在i上的節點而言,執行2倍擴容後,其下標或者仍然爲i,或者爲i+n。

所以遷移以前首先遍歷鏈表,將鏈表中的節點分爲兩個部分:遷移後下標值不一致和遷移後下標值一致,而且以一致的首節點做爲分界線,也就是lastRun變量。runBit爲0,意味着lastRun和以後的部分,遷移後下標不變;runBit不爲0,意味着lastRun和以後的部分,遷移後下標變爲i+n。

遍歷首節點到lastRun節點之間的部分,計算其遷移後的下標,構建新的node對象,而且造成鏈表。然後添加到新的數組中

協助擴容

在執行元素更新操做時,若是槽位上的節點爲ForwardingNode,則意味着當前容器正在擴容,則須要進行協助擴容,也就是方法helpTransfer的內容。代碼以下

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        if (tab != null && (f instanceof ForwardingNode) &&
            (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
            int rs = resizeStamp(tab.length);
            while (nextTab == nextTable && table == tab &&
                   (sc = sizeCtl) < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    transfer(tab, nextTab);
                    break;
                }
            }
            return nextTab;
        }
        return table;
    }

這一段代碼和addCounter中的擴容判斷部分徹底一致。

首先仍然是對當前數組長度計算蓋戳標記,也就是resizeStamp。其後在while循環中判斷是否要進行協助。while條件nextTab == nextTable && table == tab && (sc = sizeCtl) < 0代表了當前正在進行擴容,須要協助。

來看第一個if判斷:

  • (sc >>> RESIZE_STAMP_SHIFT) != rs意味着數組長度已經發生變化,擴容可能已經結束,不須要協助。
  • transferIndex <= 0意味着原始數組已經沒有能夠分配的擴容區域,不須要協助
  • sc == rs + 1 || sc == rs + MAX_RESIZERS這個條件永遠不會達成,屬於bug。具體能夠看https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8214427

若是確認須要協助,就來到第二個if。經過CAS的方式,增長了一個協助線程數量,而後執行遷移方法。

遍歷

遍歷的實現難度主要是在於遍歷的過程當中元素可能會新增或者刪除,或者遇到擴容的狀況。分狀況分析:

  • 遍歷時容器沒有變化
  • 遍歷時容器元素有新增或者刪除
  • 遍歷時容器正在擴容

遍歷是經過生成迭代器方式進行,主要三個方法keySet,valueSet,entrySet。可是遍歷的機制都是相同的,具體的實現都是依賴java.util.concurrent.ConcurrentHashMap.Traverser實現的迭代器。

首先來看下該類的重要屬性

Node<K,V>[] tab;//當前迭代器須要遍歷的數組 
Node<K,V> next; //迭代器next方法將要返回的值
TableStack<K,V> stack, spare; // 在遍歷過程當中遇到ForwardingNodes節點時,存儲當前遍歷信息的對象
int index; //下一個要遍歷的槽位的下標
int baseIndex; //初始槽位數組的當前遍歷下標
int baseLimit;  //初始槽位數組的遍歷下標的終值
final int baseSize; //初始遍歷數組的大小

從迭代器的tab屬性能夠推測出迭代的取值是從tab中來定位對應的槽位的。而從baseLimit屬性則能夠推測出遍歷的是從下標0開始的。而baseSize是初始數組的大小且爲final,意味着遍歷的範圍只針對初始數組。結合以上三點,能夠獲得遍歷的第一個原則.

遍歷是以迭代器初始化入參的數組爲依據,從下標0開始,遍歷到baseLimit截止。

關於迭代器的可見性,在遍歷的時候,容器元素可能添加或者是刪除,對於在遍歷下標以前的槽位,元素的添加或者刪除是不可見的,也不關心。而在遍歷下標以後的槽位上的元素新增刪除,在遍歷到具體的槽位時便可發現。對槽位的讀取,上面介紹過,採用的是volatile的方式,所以均可以看到最新的數據。

最複雜的狀況要屬在遍歷的時候遇到容器擴容的狀況。迭代器的最基本保證就是不能遍歷到重複的元素。可是容器的擴容的時候,下標i的節點會被從新分配到ii+n(原數組長度)的位置。也就是i+n-1位置上會有部分本來數組上i-1的元素,若是遍歷到這個槽位,則會致使重複的元素在遍歷中出現。

這邊以圖的形式更容易來講明,首先見下圖

下標0-3均已遍歷過,在遍歷下標4的槽位時發現了該節點是一個ForwardingNode節點,這意味着該數組上剩餘的槽位上的節點均已遷移到新的數組中。兩個數組中相同顏色的槽位意味着存在節點的遷移關係。好比槽位4上的節點就會遷移到新數組的槽位4和槽位12中。而灰色的部分意味着存在着已經遍歷過的槽位。顯然,重新數組的下標4開始遍歷,一旦遍歷到8-11槽位,就會遍歷到重複的數據,這顯然是不容許的。

ConcurrentHashMap的作法就是仍然遍歷原始數組,可是發現槽位節點是ForwardingNode,則遍歷ForwardingNode節點指向的數組,而且只遍歷其ii+n槽位的數據。而後迴歸原始數組,繼續這個流程。這樣的作法,就能避免遍歷到新數組中可能存在重複數據的槽位。固然,同時也忽略了這些槽位上新增的數據,可是至少保證了數據的正確性。

知道了算法思路,再來看代碼就好理解多了。私覺得,這段代碼算是最很差理解的部分了(排除紅黑樹)。

final Node<K,V> advance() {
            Node<K,V> e;
            if ((e = next) != null)
                e = e.next;
            for (;;) {
                Node<K,V>[] t; int i, n;  // must use locals in checks
                if (e != null)
                    return next = e;
                if (baseIndex >= baseLimit || (t = tab) == null ||
                    (n = t.length) <= (i = index) || i < 0)//標記1
                    return next = null;
                if ((e = tabAt(t, i)) != null && e.hash < 0) {//標記2
                    if (e instanceof ForwardingNode) {
                        tab = ((ForwardingNode<K,V>)e).nextTable;//標記3
                        e = null;
                        pushState(t, i, n);
                        continue;
                    }
                    else if (e instanceof TreeBin)
                        e = ((TreeBin<K,V>)e).first;
                    else
                        e = null;
                }
                if (stack != null)  //標記4
                    recoverState(n);
                else if ((index = i + baseSize) >= n)  //標記5
                    index = ++baseIndex; // visit upper slots if present
            }
        }

方法advance用於肯定next方法能夠返回的值,也就是肯定next屬性的值。經過標記1的代碼,i = index,能夠肯定本次須要尋找的槽位,經過標記2的代碼e = tabAt(t, i)獲取到槽位上的節點。

若是節點是ForwardingNode類型,則意味該槽位和後續的槽位都已經遷移完畢了,由於遷移的時候是從數組的末尾向前開始的。此時將須要遍歷的數組切換爲本次擴容後的數組,也就是代碼tab =((ForwardingNode<K,V>)e).nextTable的含義。切換完成後,保存此時的遍歷狀態信息,也就是方法pushState的內容,來看下具體的代碼

private void pushState(Node<K,V>[] t, int i, int n) {
            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 = s;
        }

這個方法的內容是經過TableStack造成一個堆棧的數據結構。每次保存遍歷狀態信息都是一次壓棧操做。爲了減低GC,提高效率,會將再也不使用的TableStack對象以反向的形式鏈接起來,鏈表頭存儲在spare屬性。當須要壓棧時,能夠先嚐試從spare獲取對象進行復用,而不是立刻新建對象。

遍歷狀態信息保存完畢後,就從擴容後的數組開始遍歷。經過標記1和2的代碼獲取了槽位i上的新的節點。此時就能夠針對該槽位進行遍歷,不過在遍歷以前,須要先肯定下一次遍歷的下標。也便是標記4的代碼內容。來看下方法recoverState。在標記4的調用中,其入參n是傳入的擴容後的數組大小。方法代碼以下

private void recoverState(int n) {
            TableStack<K,V> s; int len;
            while ((s = stack) != null && (index += (len = s.length)) >= n) {
                n = len;
                index = s.index;
                tab = s.tab;
                s.tab = null;
                TableStack<K,V> next = s.next;
                s.next = spare; // save for reuse
                stack = next;
                spare = s;
            }
            if (s == null && (index += baseSize) >= n)
                index = ++baseIndex;
        }

在擴容後的數組第一次進入該方法,實際的做用就是將index的值從i增長到i+n。也就是代碼index += (len = s.length))>= n的做用。第一次進入的時候,這個表達式爲false。第二次進入的時候則爲true。那就意味着上次壓棧的TableStack保存的舊的數組和遍歷下標在新的數組中對應的兩個下標位置ii+n都遍歷完畢了。此時進行一個彈棧操做,而且將須要遍歷的數組還原爲舊的數組,下標和長度信息也還原爲壓棧時的狀況。

一直執行彈棧操做,直到棧空或者再次在某一個擴容數組上index處於有效值,也就是(index += (len = s.length)) < n爲真。index是有效值,則遍歷該數組該下標的槽位上的節點。若是棧空,則意味着遍歷回到了初始數組上,也就是s == null條件成立,此時將index的值加1,也就是index = ++baseIndex,而後繼續遍歷。

而下一個槽位上的節點,也會是ForwardingNode類型,重複這個流程,直到初始數組遍歷完畢。


文章原創首發於公衆號:林斌說Java,轉載請註明來源,謝謝。 歡迎掃碼關注

相關文章
相關標籤/搜索