原文:http://www.javashuo.com/article/p-vuavzojo-en.htmljava
下面咱們來模擬一下多線程場景下擴容會出現的問題:安全
假設在擴容過程當中舊hash桶中有一個單鏈表,單鏈表中只有一個節點A,也就是e引用的對象。新hash桶中有一個單鏈表,單鏈表中的節點是B->C,也就是newTable[i]引用的對象。多線程
單線程擴容
若是隻有一個線程在執行擴容:
- 執行到第 3 行next = e.next的時候next == null
- 從第 5 行到第 9 行會將A節點按照頭插法插入到newTable[i]所引用的單鏈表中,此時newTable[i]所引用的單鏈表中的節點是A->B->C
- 第 11 行e = next會將next賦值給e,因此e == null
- 這時候循環就結束了,整個擴容過程當中毫無問題dom
多線程擴容
若是是多個線程同時在擴容,咱們以T1線程的擴容過程爲主視角,T2和T3線程只是會在T1線程擴容過程當中搗亂的:
- T1線程執行到第 7 行e.next = newTable[i]的時候會使得 e.next == B
- 此時T2線程過來搗亂了,執行到第 3 行next = e.next,那麼會使得next == B,此時T2線程的使命結束了,下面不去考慮T2線程了
- T1線程執行到第 9 行newTable[i] = e的時候,使用頭插法將A插入到newTable[i]所引用的單鏈表中,此時newTable[i]所引用的單鏈表中的節點是A->B->C
- T1線程繼續執行到 11 行e = next,將使得e == B,因爲e != null,因此循環將繼續
- T1線程開啓新的一輪循環,執行到第 3 行next = e.next的時候由於B.next == C,因此next == C
- 因爲e == B,newTable[i] == A,當T1線程執行到第 7 行e.next = newTable[i]的時候,將致使A.next == B, B.next == Aide
當執行到這一步的時候,你們會發現好像看見了一個環,離真相愈來愈近了,下面咱們兩種狀況來繼續執行下去:this
沒有T3線程介入,致使get請求死循環
T1線程繼續向下執行到第 11 行e = next,將使得e == C,將繼續進行下一輪循環
T1在這一輪新的循環中沒有其餘線程介入,這一輪執行完畢以後將跳出循環,而此時newTable[i]所引用的單鏈表會造成一個閉環 spa
這時候若是用戶發送一個get(A)的請求,將致使get請求發生死循環
有T3線程介入,致使T1線程擴容過程發生死循環
當T1線程執行到第 7 行e.next = newTable[i]的時候會使得 e.next == A
此時T3線程過來搗亂了,執行到第 3 行next = e.next,那麼會使得next == A,此時T3線程的使命結束了,下面不去考慮T2線程了
此時A.next == B, B.next == A, next == A,T1線程繼續往下執行next指針會在A和B之間無線循環,致使T1擴容過程當中發生死循環
.net
import java.util.HashMap; import java.util.Map; import java.util.UUID; public class HashMapTest { public static void main(String[] args) throws Exception { HashMap<String,String> map = new HashMap<String, String>(); TestDeadLock t1 = new TestDeadLock(map); t1.start(); TestDeadLock t2 = new TestDeadLock(map); t2.start(); TestDeadLock t3 = new TestDeadLock(map); t3.start(); } } class TestDeadLock extends Thread { private HashMap<String,String> map; public TestDeadLock(HashMap<String, String> map) { super(); this.map = map; } @Override public void run() { for (int i = 0; i<500000; i++) { map.put(UUID.randomUUID().toString(), UUID.randomUUID().toString()); System.out.println("Running ~~"); } } }
main方法執行到一半後不會再打印」Running ~~」,而且方法不會執行結束,因此判斷擴容過程形成死循環了。線程
JDK 1.7 HashMap擴容致使死循環的主要緣由
HashMap擴容致使死循環的主要緣由在於擴容後鏈表中的節點在新的hash桶使用頭插法插入。指針
新的hash桶會倒置原hash桶中的單鏈表,那麼在多個線程同時擴容的狀況下就可能致使產生一個存在閉環的單鏈表,從而致使死循環。
JDK 1.8 HashMap擴容不會形成死循環的緣由
在JDK 1.8中執行上面的擴容死循環代碼示例就不會發生死循環,咱們能夠理解爲在JDK 1.8 HashMap擴容不會形成死循環,但仍是須要理論依據纔有信服力。
首先經過上面的分析咱們知道JDK 1.7中HashMap擴容發生死循環的主要緣由在於擴容後鏈表倒置以及鏈表過長。
那麼在JDK 1.8中HashMap擴容不會形成死循環的主要緣由就從這兩個角度去分析一下。
因爲擴容是按兩倍進行擴,即 N 擴爲 N + N,所以就會存在低位部分 0 - (N-1),以及高位部分 N - (2N-1), 因此在擴容時分爲 loHead (low Head) 和 hiHead (high head)。
而後將原hash桶中單鏈表上的節點按照尾插法插入到loHead和hiHead所引用的單鏈表中。
因爲使用的是尾插法,不會致使單鏈表的倒置,因此擴容的時候不會致使死循環。
經過上面的分析,不難發現循環的產生是由於新鏈表的順序跟舊的鏈表是徹底相反的,因此只要保證建新鏈時仍是按照原來的順序的話就不會產生循環。
若是單鏈表的長度達到 8 ,就會自動轉成紅黑樹,而轉成紅黑樹以前產生的單鏈表的邏輯也是藉助loHead (low Head) 和 hiHead (high head),採用尾插法。而後再根據單鏈表生成紅黑樹,也不會致使發生死循環。
這裏雖然JDK 1.8 中HashMap擴容的時候不會形成死循環,可是若是多個線程同時執行put操做,可能會致使同時向一個單鏈表中插入數據,從而致使數據丟失的。
因此不管是JDK 1.7 仍是 1.8,HashMap線程都是不安全的,要使用線程安全的Map能夠考慮ConcurrentHashMap。