1. 程式人生 > >漫畫:高並發下的HashMap

漫畫:高並發下的HashMap

urn BL0 指向 遍歷 java代碼 LG 不存在 技術 hsl

這一期我們來講解高並發環境下,HashMap可能出現的致命問題。

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

技術分享圖片

HashMap的容量是有限的。當經過多次元素插入,使得HashMap達到一定飽和度時,Key映射位置發生沖突的幾率會逐漸提高。

這時候,HashMap需要擴展它的長度,也就是進行Resize

技術分享圖片

影響發生Resize的因素有兩個:

1.Capacity

HashMap的當前長度。上一期曾經說過,HashMap的長度是2的冪。

2.LoadFactor

HashMap負載因子,默認值為0.75f。

衡量HashMap是否進行Resize的條件如下:

HashMap.Size >= Capacity * LoadFactor

技術分享圖片

技術分享圖片

1.擴容

創建一個新的Entry空數組,長度是原數組的2倍。

2.ReHash

遍歷原Entry數組,把所有的Entry重新Hash到新數組。為什麽要重新Hash呢?因為長度擴大以後,Hash的規則也隨之改變。

讓我們回顧一下Hash公式:

index = HashCode(Key) & (Length - 1)

當原數組長度為8時,Hash運算是和111B做與運算;新數組長度為16,Hash運算是和1111B做與運算。Hash結果顯然不同。

Resize前的HashMap:

技術分享圖片

Resize後的HashMap:

技術分享圖片

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

整體情況如圖所示:

技術分享圖片

接下來執行下面的三行,用頭插法把Entry2插入到了線程B的數組的頭結點:

技術分享圖片

整體情況如圖所示:

技術分享圖片

第三次循環開始,又執行到紅框的代碼:

技術分享圖片

e = Entry3

next = Entry3.next = 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在並發的情況下可能會形成鏈表環。

漫畫:高並發下的HashMap