1、resize機制
HashMap的擴容機制就是重新申請一個容量是當前的2倍的桶數組,然后將原先的記錄逐個重新映射到新的桶里面,然后將原先的桶逐個置為null使得引用失效。后面會講到,HashMap之所以線程不安全,就是resize這里出的問題。
為什么HashMap線程不安全
上面說到,HashMap會進行resize操作,在resize操作的時候會造成線程不安全。下面將舉兩個可能出現線程不安全的地方。
1、put的時候導致的多線程數據不一致。
這個問題比較好想象,比如有兩個線程A和B,首先A希望插入一個key-value對到HashMap中,首先計算記錄所要落到的桶的索引坐標,然后獲取到該桶里面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A一樣執行,只不過線程B成功將記錄插到了桶里面,假設線程A插入的記錄計算出來的桶索引和線程B要插入的記錄計算出來的桶索引是一樣的,那么當線程B成功插入之后,線程A再次被調度運行時,它依然持有過期的鏈表頭但是它對此一無所知,以至於它認為它應該這樣做,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,造成了數據不一致的行為。
2、另外一個比較明顯的線程不安全的問題是HashMap的get操作可能因為resize而引起死循環(cpu100%),具體分析如下:
正常的hashmap表應該是這樣的:
但是 :
並發下的Rehash,假設我們有兩個線程。我用紅色和淺藍色標注了一下。
我們再回頭看一下我們的 transfer代碼中的這個細節:
而我們的線程二執行完成了。於是我們有下面的這個樣子。
注意,因為Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。我們可以看到鏈表的順序被反轉后。
線程一被調度回來執行。
- 先是執行 newTalbe[i] = e;
- 然后是e = next,導致了e指向了key(7),
- 而下一次循環的next = e.next導致了next指向了key(3)
3)一切安好。
線程一接着工作。把key(7)摘下來,放到newTable[i]的第一個,然后把e和next往下移。
4)環形鏈接出現。
e.next = newTable[i] 導致 key(3).next 指向了 key(7)
注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。
於是,當我們的線程一調用到,HashTable.get(11)時,悲劇就出現了——Infinite Loop。