1. 程式人生 > >談談HashMap執行緒不安全的體現

談談HashMap執行緒不安全的體現

我們先回顧一下HashMap。HashMap是一個數組連結串列,當一個key/Value對被加入時,首先會通過Hash演算法定位出這個鍵值對要被放入的桶,然後就把它插到相應桶中。如果這個桶中已經有元素了,那麼發生了碰撞,這樣會在這個桶中形成一個連結串列。一般來說,當有資料要插入HashMap時,都會檢查容量有沒有超過設定的thredhold,如果超過,需要增大HashMap的尺寸,但是這樣一來,就需要對整個HashMap裡的節點進行重雜湊操作。關於HashMap的重雜湊操作本文不再詳述,讀者可以參考《Map 綜述(一):徹頭徹尾理解 HashMap》一文。在此,筆者藉助陳皓的《疫苗:JAVA HASHMAP的死迴圈》

一文說明HashMap執行緒不安全的典型表現 —— 死迴圈

  HashMap重雜湊的關鍵原始碼如下:

 /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {

        // 將原陣列 table 賦給陣列 src
        Entry[] src = table;
        int newCapacity = newTable.length;

        // 將陣列 src 中的每條鏈重新新增到 newTable 中
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;   // src 回收

                // 將每條鏈的每個元素依次新增到 newTable 中相應的桶中
                do {
                    Entry<K,V> next = e.next;

                    // e.hash指的是 hash(key.hashCode())的返回值;
                    // 計算在newTable中的位置,注意原來在同一條子鏈上的元素可能被分配到不同的桶中
                    int i = indexFor(e.hash, newCapacity);   
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

1、單執行緒環境下的重雜湊過程演示

           HashMap-rehash1.jpg-68kB

  單執行緒情況下,rehash 不會出現任何問題,如上圖所示。假設hash演算法就是最簡單的 key mod table.length(也就是桶的個數)。最上面的是old hash表,其中的Hash表桶的個數為2, 所以對於 key = 3、7、5 的鍵值對在 mod 2以後都衝突在table[1]這裡了。接下來的三個步驟是,Hash表resize成4,然後對所有的鍵值對重雜湊的過程。

2、多執行緒環境下的重雜湊過程演示

  假設我們有兩個執行緒,我用紅色和淺藍色標註了一下,被這兩個執行緒共享的資源正是要被重雜湊的原來1號桶中的Entry鏈。我們再回頭看一下我們的transfer程式碼中的這個細節:

do {
    Entry<K,V> next = e.next;       // <--假設執行緒一執行到這裡就被排程掛起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

  而我們的執行緒二執行完成了,於是我們有下面的這個樣子:

           HashMap-rehash2.jpg-39.8kB

  注意,在Thread2重雜湊後,Thread1的指標e和指標next分別指向了Thread2重組後的連結串列(e指向了key(3),而next指向了key(7))。此時,Thread1被排程回來執行:Thread1先是執行 newTalbe[i] = e;然後是e = next,導致了e指向了key(7),而下一次迴圈的next = e.next導致了next指向了key(3),如下圖所示:

           HashMap-rehash3.jpg-35.9kB

  這時,一切安好。Thread1有條不紊的工作著:把key(7)摘下來,放到newTable[i]的第一個,然後把e和next往下移,如下圖所示:

           HashMap-rehash4.jpg-45.7kB

  在此時,特別需要注意的是,當執行e.next = newTable[i]後,會導致 key(3).next 指向了 key(7),而此時的key(7).next 已經指向了key(3),環形連結串列就這樣出現了,如下圖所示。於是,當我們的Thread1呼叫HashMap.get(11)時,悲劇就出現了 —— Infinite Loop。

           HashMap-rehash5.jpg-41.1kB

  這是HashMap在併發環境下使用中最為典型的一個問題,就是在HashMap進行擴容重雜湊時導致Entry鍊形成環。一旦Entry鏈中有環,勢必會導致在同一個桶中進行插入、查詢、刪除等操作時陷入死迴圈。