在 JDK7 版本下,不少人都知道 HashMap 會有鏈表成環的問題,但大多數人只知道,是多線程引發的,至於具體細節的緣由,和 JDK8 中如何解決這個問題,不多有人說的清楚,百度也幾乎看不懂,本文就和你們聊清楚兩個問題:1:JDK7 中 HashMap 成環緣由,2:JDK8 中是如何解決的。java
1:HashMap 擴容時。
2:多線程環境下。面試
在擴容的 transfer 方法裏面,有三行關鍵的代碼,以下:數組
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { //e爲空時循環結束 while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); // 成環的代碼主要是在這三行代碼 // 首先插入是從頭開始插入的 e.next = newTable[i]; newTable[i] = e; e = next; } } }
假設原來在數組 1 的下標位置有個鏈表,鏈表元素是 a->b->null,如今有兩個線程同時執行這個方法,咱們先來根據線程 1 的執行狀況來分別分析下這三行代碼:安全
e.next = newTable[i];
newTable 表示新的數組,newTable[i] 表示新數組下標爲 i 的值,第一次循環的時候爲 null,e 表示原來鏈表位置的頭一個元素,是 a,e.next 是 b,
e.next = newTable[i] 的意思就是拿出 a 來,而且使 a 的後一個節點是 null,以下圖 1 的位置:
數據結構
newTable[i] = e;
就是把 a 賦值給新數組下標爲 1 的地方,以下圖 2 的位置:
多線程
e = next;
next 的值在 while 循環一開始就有了,爲:Entry<K,V> next = e.next; 在此處 next 的值就是 b,把 b 賦值給 e,接着下一輪循環。線程
從 b 開始下一輪循環,重複 一、二、3,注意此時 e 是 b 了,而 newTable[i] 的值已經不是空了,已是 a 了,因此 1,2,3 行代碼執行下來,b 就會插入到 a 的前面,以下圖 3 的位置:
這個就是線程 1 的插入節奏。3d
重點來了,假設線程 1 執行到如今的時候,線程 2 也開始執行,線程 2 是從 a 開始執行 一、二、三、4 步,此時數組上面鏈表已經造成了 b->a->null,線程 2 拿出 a 再次執行 一、二、三、4,就會把 a 放到 b 的前面,你們能夠想象一下,結果是以下圖的:
從圖中能夠看出,有兩個相同的 a 和兩個相同的 b,這就是你們說的成環,本身通過不斷 next 最終指向本身。code
注意!!!這種解釋看似好像頗有道理,但其實是不正確的,網上不少這種解釋,這種解釋最致命的地方在於 newTable 不是共享的,線程 2 是沒法在線程 1 newTable 的基礎上再進行遷移數據的,一、二、3 都沒有問題,但 4 有問題,最後的結論也是有問題的blog
由於 newTable 是在擴容方法中新建的局部變量,方法的局部變量線程之間確定是沒法共享的,因此以上解釋是有問題的,是錯誤的。
那麼真正的問題出如今那裏呢,其實線程 1 完成 一、二、三、4 步後就出現問題了,以下圖:
總結一下產生這個問題的緣由:
接下來咱們來看下 JDK8 是怎麼解決這個問題。
JDK 8 中擴容時,已經沒有 JDK7 中的 transfer 方法了,而是本身從新寫了擴容方法,叫作 resize,鏈表從老數組拷貝到新數組時的代碼以下:
//規避了8版本如下的成環問題 else { // preserve order // loHead 表示老值,老值的意思是擴容後,該鏈表中計算出索引位置不變的元素 // hiHead 表示新值,新值的意思是擴容後,計算出索引位置發生變化的元素 // 舉個例子,數組大小是 8 ,在數組索引位置是 1 的地方掛着一個鏈表,鏈表有兩個值,兩個值的 hashcode 分別是是9和33。 // 當數組發生擴容時,新數組的大小是 16,此時 hashcode 是 33 的值計算出來的數組索引位置仍然是 1,咱們稱爲老值 // hashcode 是 9 的值計算出來的數組索引位置是 9,就發生了變化,咱們稱爲新值。 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // java 7 是在 while 循環裏面,單個計算好數組索引位置後,單個的插入數組中,在多線程狀況下,會有成環問題 // java 8 是等鏈表整個 while 循環結束後,纔給數組賦值,因此多線程狀況下,也不會成環 do { next = e.next; // (e.hash & oldCap) == 0 表示老值鏈表 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // (e.hash & oldCap) == 0 表示新值鏈表 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 老值鏈表賦值給原來的數組索引位置 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 新值鏈表賦值到新的數組索引位置 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } }
解決辦法其實代碼中的註釋已經說的很清楚了,咱們總結一下:
JDK8 是等鏈表整個 while 循環結束後,纔給數組賦值,此時使用局部變量 loHead 和 hiHead 來保存鏈表的值,由於是局部變量,因此多線程的狀況下,確定是沒有問題的。
爲何有 loHead 和 hiHead 兩個新老值來保存鏈表呢,主要是由於擴容後,鏈表中的元素的索引位置是可能發生變化的,代碼註釋中舉了一個例子:
數組大小是 8 ,在數組索引位置是 1 的地方掛着一個鏈表,鏈表有兩個值,兩個值的 hashcode 分別是是 9 和 33。當數組發生擴容時,新數組的大小是 16,此時 hashcode 是 33 的值計算出來的數組索引位置仍然是 1,咱們稱爲老值(loHead),而 hashcode 是 9 的值計算出來的數組索引位置倒是 9,不是 1 了,索引位置就發生了變化,咱們稱爲新值(hiHead)。
你們能夠仔細看一下這幾行代碼,很是巧妙。
本文主要分析了 HashMap 鏈表成環的緣由和解決方案,你學會了嗎?
想知道 HashMap 底層數據結構是什麼樣的麼?想了解 ConcurrentHashMap 是如何保證線程安全的麼?想閱讀更多 JUC 的源碼麼,請關注個人新課:面試官系統精講Java源碼及大廠真題,帶你一塊兒閱讀 Java 核心源碼,瞭解更多使用場景,爲升職加薪作好準備。