之前我已經寫過關於HashMap的內容了:http://www.cnblogs.com/wang-meng/p/7545725.html
我們都知道HashMap是線程不安全的, 如果多線程來訪問會有什么問題呢? 答案是會造成死鎖。
接下來我們就分析下為何會造成死鎖。
說到HashMap中死鎖的情況, 我們就必須要先講下resize()方法, 顧名思義, 這個方法就是來擴容的。
當HashMap的size超過 thredshold時, 就需要擴容了。 當我們put時:
(截圖代碼為JDK7 HashMap源碼)
首先,我們需要知道幾個最基本的概念: Entry<K,V>[] table的初始化長度length(默認值是16),Load factor為負載因子(默認值是0.75),threshold是HashMap所能容納的最大數據量的Entry(鍵值對)個數。size是HashMap中實際存在 的鍵值對數量。threshold = length * Load factor。也就是說,在數組定義好長度之后,負載因子越大,所能容納的鍵值對個數越多。
接着我們直接看上面的代碼, 當size >= threshold且table[bucketIndex]不為空就會觸發resize操作。 然后看resize()方法:
這里重點就是transfer方法, 接着我們來看transfer方法:
第一: 遍歷舊的table
第二: 將舊的table中每個元素重新計算hash值, 然后賦予新的table中
具體我們將用圖示的方法來解析:
單線程擴容:
假設:hash算法就是簡單的key與length(數組長度)求余。
hash表長度為2,如果不擴容, 那么元素key為3,5,7按照計算(key%table.length)的話都應該碰撞到table[1]上
擴容:hash表長度會擴容為4
重新hash,key=3 會落到table[3]上(3%4=3), 當前e.next為key(7), 繼續while循環
重新hash,key=7 會落到table[3]上(7%4=3), 產生碰撞, 這里采用的是頭插入法,所以key=7的Entry會排在key=3前面(這里可以具體看while語句中代碼)
當前e.next為key(5), 繼續while循環
重新hash,key=5 會落到table[1]上(5%4=1), 當前e.next為null, 跳出while循環, resize結束
如題如圖所示:
多線程擴容:
這里我們先把核心代碼搬出來, 方便查看
while(null != e) {
Entry<K,V> next = e.next; //第一行
int i = indexFor(e.hash, newCapacity); //第二行
e.next = newTable[i]; //第三行
newTable[i] = e; //第四行
e = next; //第五行
}
去掉了一些冗余的代碼, 層次結構更加清晰了。
第一行:記錄odl hash表中e.next
第二行:rehash計算出數組的位置(hash表中桶的位置)
第三行:e要插入鏈表的頭部, 所以要先將e.next指向new hash表中的第一個元素
第四行:將e放入到new hash表的頭部
第五行: 轉移e到下一個節點, 繼續循環下去
核心代碼如上所說, 下面就是多線程同時put的情況了, 然后同時進入transfer方法中:
假設這里有兩個線程同時執行了put()
操作,並進入了transfer()
環節
while(null != e) { Entry<K,V> next = e.next; //線程1執行到這里被調度掛起了 e.next = newTable[i]; newTable[i] = e; e = next; }
那么現在的狀態為:
從上面的圖我們可以看到,因為線程1的 e 指向了 key(3),而 next 指向了 key(7),在線程2 rehash 后,就指向了線程2 rehash 后的鏈表。
然后線程1被喚醒了:
- 執行
e.next = newTable[i]
,於是 key(3)的 next 指向了線程1的新 Hash 表,因為新 Hash 表為空,所以e.next = null
, - 執行
newTable[i] = e
,所以線程1的新 Hash 表第一個元素指向了線程2新 Hash 表的 key(3)。好了,e 處理完畢。 - 執行
e = next
,將 e 指向 next,所以新的 e 是 key(7)
然后該執行 key(3)的 next 節點 key(7)了:
- 現在的 e 節點是 key(7),首先執行
Entry<K,V> next = e.next
,那么 next 就是 key(3)了 - 執行
e.next = newTable[i]
,於是key(7) 的 next 就成了 key(3) - 執行
newTable[i] = e
,那么線程1的新 Hash 表第一個元素變成了 key(7) - 執行
e = next
,將 e 指向 next,所以新的 e 是 key(3)
這時候的狀態圖為:
然后又該執行 key(7)的 next 節點 key(3)了:
- 現在的 e 節點是 key(3),首先執行
Entry<K,V> next = e.next
,那么 next 就是 null - 執行
e.next = newTable[i]
,於是key(3) 的 next 就成了 key(7) - 執行
newTable[i] = e
,那么線程1的新 Hash 表第一個元素變成了 key(3) - 執行
e = next
,將 e 指向 next,所以新的 e 是 key(7)
這時候的狀態如圖所示:
很明顯,環形鏈表出現了!!當然,現在還沒有事情,因為下一個節點是 null,所以
transfer()
就完成了,等put()
的其余過程搞定后,HashMap 的底層實現就是線程1的新 Hash 表了。