上篇分析了HashMap的設計思想以及Java7和Java8源碼上的實現,固然還有一些"坑"還沒填完,好比你們都知道HashMap是線程不安全的數據結構,多線程狀況下HashMap會引發死循環引用,它是怎麼產生的?Java8引入了紅黑樹,那是怎麼提升效率的?本篇先填第一個坑,仍是以圖解的形式加深理解。html
經過上一篇的總體學習,能夠知道當存入的鍵值對超過HashMap的閥值時,HashMap會擴容,即建立一個新的數組,並將原數組裏的鍵值對"轉移"到新的數組中。在「轉移」的時候,會根據新的數組長度和要轉移的鍵值對key值從新計算在新數組中的位置。重溫下Java7中負責"轉移"功能的代碼java
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。擴容前的模擬堆內存狀況如圖bash
Thread1獲得執行權(Thread2被掛起),Thread1往hashMap裏put第13個鍵值對的時候判斷超過閥值,執行擴容操做,Thread1建立了一個新數組,還沒來得及轉移舊鍵值對的時候,系統時間片反手切到Thread2(Thread1被掛起),整個過程用圖表示markdown
能夠看到Thread1只是建立了個新數組,還沒來得及轉移就被掛起了,新數組沒有內容,此時在Thread1的視角認的是e是石頭,next是蓋倫;此時的模擬內存圖狀況數據結構
再看下Thread2的操做,一樣Thread2往hashMap裏put第13個鍵值對的時候判斷超過閥值,執行擴容操做,Thread2先建立一個新數組,不一樣的是,Thread2運氣好,在時間片輪換前轉移工做也走完了。第一次遍歷多線程
第二次遍歷oop
此時模擬的內存狀況源碼分析
能夠看到此時對於蓋倫來講,他的next是石頭;對於石頭來講,它的next爲null,隱患就此埋下。接下來時間片又切到Thread1(停了半天終於輪到我出場了),先看下Thread1的處境
結合代碼分析以下
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
第七步:
第八步:
第九步:
第10步:
到這終於看到蓋倫和石頭"互指",水乳交融。
那這會帶來什麼後果呢?後續操做新數組的5號坑會進入死循環(注意,操做其餘坑並不會有問題),例如Java7 put操做
Java7 get操做會執行getEntry,一樣會引發死循環。
到此,Java7多線程操做HashMap可能造成死循環的緣由剖析完成。
經過上一篇的學習可知,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。