HashMap為什么線程不安全(死循環+數據丟失過程分析)


jdk1.7中的HashMap

擴容部分源碼

擴容分為兩步
1.創建一個數組容量為原來2倍的HashMap
2.遍歷舊的Entry數組,把所有的Entry重新Hash到新數組

在開始之前我們先看一下擴容部分的源碼

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;     
    // 遍歷舊數組中的每個桶
    for (Entry<K,V> e : table) {      
        // 遍歷桶中的元素Entry(是一個鏈表)
        while(null != e) {
            Entry<K,V> next = e.next;
            //如果是重新Hash,則需要重新計算hash值  
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //計算該Entry在新table數組中對應桶的下標Index
            /*
             * indexFor中的代碼是return h & (length-1);  
             *可見hash對應的數組下標index是與hashmap的大小有關的,所以擴容不能直接把舊數據復制過來,需要遍歷重新計算下標index的值
            */
            int i = indexFor(e.hash, newCapacity);         
            //頭插法
            e.next = newTable[i];
            newTable[i] = e;
            //繼續下一個元素(next是再上面就存下來了的,是原來鏈表的下一個元素)
            e = next;
        }
    }
}

擴容導致死循環

假設的前提條件:

  1. index求解為簡單的用key mod 數組的長度
  2. 最開始size=2 , key=3,7,5,則由假設1可知3 7 5 都在table[1]中
  3. 然后進行resize,使size變為4

未resize前的數據結構:

單線程

如果在單線程下根據上方transfer的源碼理解(遍歷+頭插法),最后擴容結果如下:

多線程

假設有兩個線程A,B都在進行擴容操作,但是線程A遍歷的時候在*處被暫時掛起了

 1    void transfer(Entry[] newTable, boolean rehash) {
 2         int newCapacity = newTable.length;
 3         for (Entry<K,V> e : table) {
 4             while(null != e) {
 5                 Entry<K,V> next = e.next;
 6                 if (rehash) {
 7                     e.hash = null == e.key ? 0 : hash(e.key);
 8                 }
 9                 int i = indexFor(e.hash, newCapacity);
10                 e.next = newTable[i];
11                 newTable[i] = e;--------* A被掛起
12                 e = next;
13             }
14         }
15     }

①此時A線程e=3 | next=7(上方代碼第5行得)| i=3 (由假設1可知3%4=3) | e.next=null(10行代碼 3.next=null)| 在執行第11行的時候被線程掛起

此時A線程的結果

A線程被掛起,B線程正常執行,完全執行完resize擴容操作后,結果如下:


★由於B執行完了,所以現在java內存中newtable和table中的Entry都是B線程執行完的最新值★
★即: newTable[1]=5 | 5.next=null | newTable[3]=7 | 7.next=3 | 3.next=null★

此時再切換回A線程,A線程重新繼續執行11行的代碼,①已經求得的臨時變量的值是沒變的。
上面①中的臨時變量繼續使用

11  newTable[i] = e;  -----(i=3,e=3帶入得newTable[3]=3)
12  e = next;         -----(next=7,帶入得e=7)

執行結果如下:

繼續循環(此時e=7 由上次循環的代碼可知):

5 next = e.next;                     -----(由上圖可知7.next=3故得next=3)
9 i = indexFor(e.hash,newCapacity);  -----(由假設1可知index=7%4=3)
10 e.next = newTable[i];             -----(由上圖可知newTable[3]=3故得e.next=3)
11 newTable[i] = e;                  -----(newTable[3]=7)
12 e = next                          -----(e=3)

結果如下:

繼續循環(此時e=3 由上次循環的代碼可知):

5 next = e.next;                     -----(由上圖可知3.next=null故得next=null)
9 i = indexFor(e.hash,newCapacity);  -----(由假設1可知index=3%4=3)
10 e.next = newTable[i];             -----(由上圖可知newTable[3]=7故得e.next=7)
11 newTable[i] = e;                  -----(newTable[3]=3)
12 e = next                          -----(e=null)

至此e=null,循環結束
結果如下:

可見這出現了一個循環鏈表,在之后只要涉及輪詢hashmap數據結構,就會在這里發生死循環。

擴容導致數據丟失

假設前提條件

  1. index求解為簡單的用key mod 數組的長度
  2. 最開始size=2 , key=7,5,3,則由假設1可知7 5 3 都在table[1]中
  3. 然后進行resize,使size變為4

    同樣兩個線程A,B都在進行擴容,A在*處被掛起
 1    void transfer(Entry[] newTable, boolean rehash) {
 2         int newCapacity = newTable.length;
 3         for (Entry<K,V> e : table) {
 4             while(null != e) {
 5                 Entry<K,V> next = e.next;
 6                 if (rehash) {
 7                     e.hash = null == e.key ? 0 : hash(e.key);
 8                 }
 9                 int i = indexFor(e.hash, newCapacity);
10                 e.next = newTable[i];
11                 newTable[i] = e;--------* A被掛起
12                 e = next;
13             }
14         }
15     }

①此時A線程e=7 | next=5(上方代碼第5行得)| i=3 (由假設1可知7%4=3) | e.next=null(10行代碼 3.next=null)| 在執行第11行的時候被線程掛起
執行結果如下:

A線程被掛起,B線程正常執行,完全執行完resize擴容操作后,結果如下:


★由於B執行完了,所以現在java內存中newtable和table中的Entry都是B線程執行完的最新值★
★即: newTable[1]=5 | 5.next=null | newTable[3]=3 | 3.next=7 | 7.next=null★
此時再切換回A線程,A線程重新繼續執行11行的代碼,①已經求得的臨時變量的值是沒變的。
上面①中的臨時變量繼續使用:①此時A線程e=7 | next=5(上方代碼第5行得)| i=3 (由假設1可知3%4=3) | e.next=null(10行代碼 7.next=null)| 在執行第11行的時候被線程掛起

11  newTable[i] = e;  -----(i=3,e=7帶入得newTable[3]=7)
12  e = next;         -----(next=5,帶入得e=5)

執行結果如下:

繼續循環(此時e=5 由上次循環的代碼可知):

5 next = e.next;                     -----(由上圖可知5.next=null故得next=null)
9 i = indexFor(e.hash,newCapacity);  -----(由假設1可知index=5%4=1)
10 e.next = newTable[i];             -----(由上圖可知newTable[1]=5故得e.next=5)
11 newTable[i] = e;                  -----(newTable[1]=5)
12 e = next                          -----(e=null)

至此e=null,循環結束
結果如下:
3元素丟失,並形成環形鏈表。並在后續操作hashmap時造成死循環。

jdk1.8中的HashMap

jdk1.8中對HashMap進行了優化,如果發生hash碰撞,不采用頭插法,改成了尾插法,因此不會出現循環鏈表的情況。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果插入的位置為null,沒有hash碰撞,則直接插入元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        .........

但是在多線程的環境下仍然不安全,當兩個線程A,B同時進行Put的操作的時候,都判斷當前沒有hash碰撞,然后同時進行直接插入,那么后面哪個線程的值Entry會把前一個線程插入的Entry給**覆蓋**掉,發生線程不安全


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM