高併發下的HashMap(執行緒不安全)
高併發下的HashMap
這些討論是在1.8之前的java下作的分析,1.8的HashMap做了很大的變化,可以保證高併發下的安全性(多執行緒)。
HashMap的容量是有限的。當經過多次元素插入,使得HashMap達到一定飽和度時,Key對映位置發生衝突的機率會逐漸提高。
這時候,HashMap需要擴充套件它的長度,也就是進行Resize。
影響發生Resize的因素有兩個:
1.Capacity
HashMap的當前長度。HashMap的長度是2的冪。2.LoadFactor
HashMap負載因子,預設值為0.75f。
衡量HashMap是否進行Resize的條件如下:
HashMap.Size >= Capacity * LoadFactor
Resize經過了兩個步驟:
1.擴容
建立一個新的Entry空陣列,長度是原陣列的2倍。
2.ReHash
遍歷原Entry陣列,把所有的Entry重新Hash到新陣列。為什麼要重新Hash呢?因為長度擴大以後,Hash的規則也隨之改變。
可以理解為我們10口之家從50平的筒子樓搬到了500平的豪華別墅,我們再也不需要6個人擠一個房間了 ,我們可以有自己的獨立空間了(人口下一次增加之前)
ReSize的過程
ReHash的Java程式碼如下:
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key) ;
}
//判斷存貯在哪個雜湊桶位置
int i = indexFor(e.hash, newCapacity);
/*
*頭插法,新的節點插在舊的佇列前面
*/
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
剛才發生的一切都是在單執行緒下發生的,但是在多執行緒的情況下,事情就發生了很大的變化:
假設一個HashMap已經到了Resize的臨界點。此時有兩個執行緒A和B,在同一時刻對HashMap進行Put操作:
此時達到Resize條件,兩個執行緒各自進行Rezie的第一步,也就是擴容:
這時候,兩個執行緒都走到了ReHash的步驟。讓我們回顧一下ReHash的程式碼:
假如此時執行緒B遍歷到Entry3物件,剛執行完紅框裡的這行程式碼,執行緒就被掛起。對於執行緒B來說:
e = Entry3 //第一次迴圈
next = Entry2 //第一次迴圈
這時候執行緒A暢通無阻地進行著Rehash,當ReHash完成後,結果如下(圖中的e和next,代表執行緒B的兩個引用):
直到這一步,看起來沒什麼毛病。接下來執行緒B恢復,繼續執行屬於它自己的ReHash。執行緒B剛才的狀態是:
e = Entry3 //第一次迴圈
next = Entry2 //第一次迴圈
當執行到上面這一行時,顯然 i = 3,因為剛才執行緒A對於Entry3的hash結果也是3。
我們繼續執行到這兩行,Entry3放入了執行緒B的陣列下標為3的位置,並且e指向了Entry2。此時e和next的指向如下:
e = Entry2 //程式碼結尾指向
next = Entry2 //程式碼開頭指向
整體情況如圖所示:
接著是新一輪迴圈,又執行到紅框內的程式碼行:
e = Entry2 //上一迴圈中指向
next = Entry3 //本次迴圈開頭指向
然後執行頭插法,變成:
第三次迴圈開始,又執行到紅框的程式碼:
此時:
e = Entry3 //上一輪結尾指向
next = Entry3.next = null //執行緒A的指向為null
這個時候,由於是頭插法,這裡產生了環
newTable[i] = Entry2
e = Entry3
Entry2.next = Entry3
Entry3.next = Entry2
此時,問題還沒有直接產生。當呼叫Get查詢一個不存在的Key,而這個Key的Hash結果恰好等於3的時候,由於位置3帶有環形連結串列,所以程式將會進入死迴圈!
總結:
1.Hashmap在插入元素過多的時候需要進行Resize,Resize的條件是
HashMap.Size >= Capacity * LoadFactor。
2.Hashmap的Resize包含擴容和ReHash兩個步驟,ReHash在併發的情況下可能會形成連結串列環。
總結原因是由於,一個執行緒使用頭插法想一個桶的連結串列的順序倒置,另一個執行緒在掛起後先按照一開始的順序遍歷,一次後又按照新的順序遍歷。便形成了環
。但是這種執行緒不安全並不是每一次都會出現,只有存在概率。
這個問題在1.8版本上有了很大的修改,當一個桶的連結串列達到8個時,會自動轉變為紅黑樹,避免了這個問題。
JDK1.8後,除了對hashmap增加紅黑樹結果外,對原有造成死鎖的關鍵原因點(新table複製採用頭插法)改進為依次在末端新增新的元素。雖然JDK1.8後新增紅黑樹改進了連結串列過長查詢遍歷慢問題和resize時出現導致put死迴圈的bug,但還是非線性安全的,比如資料丟失等等。因此多執行緒情況下還是建議使用concurrenthashmap。
文章絕大部分內容來自公眾號《程式設計師小灰》,學習中加入了自己的一些理解,舔著臉標為原創,不喜可揍,痛揍。