談談HashMap執行緒不安全的體現
我們先回顧一下HashMap。HashMap是一個數組連結串列,當一個key/Value對被加入時,首先會通過Hash演算法定位出這個鍵值對要被放入的桶,然後就把它插到相應桶中。如果這個桶中已經有元素了,那麼發生了碰撞,這樣會在這個桶中形成一個連結串列。一般來說,當有資料要插入HashMap時,都會檢查容量有沒有超過設定的thredhold,如果超過,需要增大HashMap的尺寸,但是這樣一來,就需要對整個HashMap裡的節點進行重雜湊操作。關於HashMap的重雜湊操作本文不再詳述,讀者可以參考《Map 綜述(一):徹頭徹尾理解 HashMap》一文。在此,筆者藉助陳皓的《疫苗:JAVA 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、單執行緒環境下的重雜湊過程演示
單執行緒情況下,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
而我們的執行緒二執行完成了,於是我們有下面的這個樣子:
注意,在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),如下圖所示:
這時,一切安好。Thread1有條不紊的工作著:把key(7)摘下來,放到newTable[i]的第一個,然後把e和next往下移,如下圖所示:
在此時,特別需要注意的是,當執行e.next = newTable[i]後,會導致 key(3).next 指向了 key(7),而此時的key(7).next 已經指向了key(3),環形連結串列就這樣出現了,如下圖所示。於是,當我們的Thread1呼叫HashMap.get(11)時,悲劇就出現了 —— Infinite Loop。
這是HashMap在併發環境下使用中最為典型的一個問題,就是在HashMap進行擴容重雜湊時導致Entry鍊形成環。一旦Entry鏈中有環,勢必會導致在同一個桶中進行插入、查詢、刪除等操作時陷入死迴圈。