併發下HashMap為什麼不是執行緒安全的?
首先看下HashMap的工作原理,我們回顧一下HashMap的結構:
HashMap的結構就是雜湊表,底層是一個數組,這個陣列中儘可能地分散所有的key,通過key的hash值得到陣列下標,然後把entry插到該陣列,假如有兩個不同的key被分到相同的下標,也就是雜湊衝突,那麼該陣列在該下標下就會形成連結串列。
為了減少衝突,我們需要時刻留意當前的size是否太大,檢查是否需要擴容,一旦超過設定的threshold,那麼就要重新增大陣列尺寸,此時所有元素都需要重新計算應該放置的下標。
擴容、rehash
上面為擴容的方法,可以看到,實際上擴容就是新建了一個數組,同時呼叫了transfer方法,給新陣列賦值後覆蓋掉原來的table。那我們來看看transfer裡面發生了什麼:
從程式碼上可以看出,原來連結串列的元素有可能已經不在原來的陣列上,也就是元素都被重新排到陣列上了。
圖解rehash
擴容前的結構:
在transfer方法中,對原來的table進行遍歷重排序,我們來模擬一下這個過程。重新排序的關鍵在於,陣列長度變了以後,重新計算得出的下標,這與雜湊演算法有關,為了便於理解,我們簡單地認為本HashMap的雜湊演算法為用key 去mod陣列的長度,這也是理解transfer如何重新排列連結串列的關鍵。
這是第一次迴圈後的結果:
第二步:
繼續遍歷,最終結果如下
以上就是在單執行緒下,重新排列後的連結串列。
多執行緒下的rehash
我們從上文看到,單執行緒下的rehash是完全沒有問題的,那麼在多執行緒下會出現什麼問題呢?我們仍然用清晰明瞭的圖來做演示
首先,我們假設有兩個程序同時進行put操作,且讓HashMap進行擴容,同時進入transfer方法
此時的狀態如下:
然後執行緒2完成了transfer方法,如下
此時,又回到執行緒1,但此時,完成第一次迴圈過程:
繼續進行下一次迴圈:
然後e = next = key(3),再繼續下一次迴圈:
當執行完:
e.next = newTable[3];
newTable[3] = e;
時,至此,迴圈連結串列形成了。