概述
上篇分析了HashMap的設計思想以及Java7和Java8原始碼上的實現,當然還有一些"坑"還沒填完,比如大家都知道HashMap是執行緒不安全的資料結構,多執行緒情況下HashMap會引起死迴圈引用,它是怎麼產生的?Java8引入了紅黑樹,那是怎麼提高效率的?本篇先填第一個坑,還是以圖解的形式加深理解。
Java7分析
通過上一篇的整體學習,可以知道當存入的鍵值對超過HashMap的閥值時,HashMap會擴容,即建立一個新的陣列,並將原陣列裡的鍵值對"轉移"到新的陣列中。在“轉移”的時候,會根據新的陣列長度和要轉移的鍵值對key值重新計算在新陣列中的位置。重溫下Java7中負責"轉移"功能的程式碼
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;
}
}
}
複製程式碼
為了加深理解,畫個圖如下
這裡假設擴容前後5號坑石頭、蓋倫、蒙多的hash值與新舊陣列長度取模運算後還是5。上篇文章也總結了,Java7擴容轉移前後連結串列順序會倒置。當只有單執行緒操作hashMap時,一切都是那麼美好,但是如果多執行緒同時操作一個hashMap,問題就來了,下面看下多執行緒操作一個hashMap在Java7原始碼下是怎樣引起死迴圈引用。
前戲是這樣的:有兩個執行緒分別叫Thread1和Thread2,它們都有操作同一個hashMap的權利,假設hashMap中的鍵值對是12個,石頭和蓋倫擴容前後的hash值與新舊陣列長度取模運算後還是5。擴容前的模擬堆記憶體情況如圖
Thread1得到執行權(Thread2被掛起),Thread1往hashMap裡put第13個鍵值對的時候判斷超過閥值,執行擴容操作,Thread1建立了一個新陣列,還沒來得及轉移舊鍵值對的時候,系統時間片反手切到Thread2(Thread1被掛起),整個過程用圖表示
可以看到Thread1只是建立了個新陣列,還沒來得及轉移就被掛起了,新陣列沒有內容,此時在Thread1的視角認的是e是石頭,next是蓋倫;此時的模擬記憶體圖情況
再看下Thread2的操作,同樣Thread2往hashMap裡put第13個鍵值對的時候判斷超過閥值,執行擴容操作,Thread2先建立一個新陣列,不同的是,Thread2運氣好,在時間片輪換前轉移工作也走完了。第一次遍歷
第二次遍歷
此時模擬的記憶體情況
可以看到此時對於蓋倫來說,他的next是石頭;對於石頭來說,它的next為null,隱患就此埋下。接下來時間片又切到Thread1(停了半天終於輪到我出場了),先看下Thread1的處境
結合程式碼分析如下
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
第七步:
第八步:
第九步:
第10步:
到這終於看到蓋倫和石頭"互指",水乳交融。
那這會帶來什麼後果呢?後續操作新陣列的5號坑會進入死迴圈(注意,操作其他坑並不會有問題),例如Java7 put操作
Java7 get操作會執行getEntry,同樣會引起死迴圈。
到此,Java7多執行緒操作HashMap可能形成死迴圈的原因剖析完成。
Java8分析
通過上一篇的學習可知,Java7轉移前後位置顛倒,而Java8轉移鍵值對前後位置不變。同樣的前戲,看下程式碼
此時模擬堆記憶體情況
Thread1的情況
這時候Thread2獲得執行權,擴容並完成轉移工作,通過上篇的學習可知,Java8在轉移前會建立兩條連結串列,即擴容後位置不加原陣列長度的lo鏈和要加原陣列長度的hi鏈,這裡假設石頭和蓋倫擴容前後都在5號坑,即這是一條lo鏈(其實就算不在同一個坑也不影響,原因就是Java8擴容前後鏈順序不變)。Thread2遍歷第一次
第二次
可以看到Thread2全程是沒有去修改石頭和蓋倫的引用關係,石頭.next是蓋倫,蓋倫.next是null。那麼Thread1得到執行權後其實只是重複了Thread2的工作。
總結
通過原始碼分析,Java7在多執行緒操作hashmap時可能引起死迴圈,原因是擴容轉移後前後連結串列順序倒置,在轉移過程中修改了原來連結串列中節點的引用關係;Java8在同樣的前提下並不會引起死迴圈,原因是擴容轉移後前後連結串列順序不變,保持之前節點的引用關係。那是不是意味著Java8就可以把HashMap用在多執行緒中呢?個人感覺即使不會出現死迴圈,但是通過原始碼看到put/get方法都沒有加同步鎖,多執行緒情況最容易出現的就是:無法保證上一秒put的值,下一秒get的時候還是原值,建議使用ConcurrentHashMap。