1. 程式人生 > >高併發下的HashMap(執行緒不安全)

高併發下的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。

文章絕大部分內容來自公眾號《程式設計師小灰》,學習中加入了自己的一些理解,舔著臉標為原創,不喜可揍,痛揍。