jdk7和8中關於HashMap和concurrentHashMap的擴容過程總結,以及HashMap死循環


題外話:為什么要hashcode進行spread? 充分使用key.hashCode()的高16位信息,保證hash分布更分散,

 

擴容操作是新建2倍於原表大小的新表,並將原表結點拷貝一份放在新表中,對原表無修改或修改很小。當原表所有結點都已被拷貝到新表中后,原表會被垃圾回收。

 

在jdk7中的HashMap實現類中,數組+鏈表。擴容操作是將原數組的結點一一進行hash計算,然后一一掛接到新數組上,所以不是基於復制結點的機制。
在jdk7中的ConcurrentHashMap實現類中,段(segment)+數組+鏈表。擴容操作是先遍歷數組元素,在每個數組元素上遍歷一遍鏈表,找到鏈表的最后n個結點(這n個結點在新數組一定屬於同一個數組位置上),把這n個結點先掛接到新數組的數組位置上,這也叫lastRun機制。至於原數組的頭結點到倒數n個結點之間的結點,再遍歷一遍,通過復制每個結點的機制掛接到新數組上。

jdk8中的HashMap實現類中,數組+鏈表/紅黑樹。擴容操作是將原數組每個結點的hash值和原數組長度進行“與”操作,結果等於0代表該結點位置不變,落在新數組的同樣位置,否則該結點在新數組的位置是[j + oldCap]上。
原因是:擴容是將數組長度擴大一倍,假如原長度是16(二進制是10000,掩碼1111),新長度是32(二進制是100000,掩碼11111),那么在計算結點所落的位置時,hash值原本是低4位參與計算,擴容后變成hash值低5位參與計算,這樣的話,當參與運算的最高位也就是第五位,是1時,必然落在擴容后的新的位置,是0時,必然位置不變,因為原數組長度16的二進制第五位是1,所以通過將結點的hash值和原數組長度進行與操作,就可以知道結點在新數組中是保持相對不變還是落在高一點的位置上。

jdk8中的ConcurrentHashMap實現類中,數組+鏈表/紅黑樹。擴容操作是多個線程參與共同完成的,相比於jdk7版本的擴容,jdk8的擴容屬於漸進式擴容,不是一蹴而就。將原數組長度為n作為擴容任務的總數,切分成m塊作為m個小任務,每個小任務有且只有一個線程來負責完成擴容(因為擴容后的數組長度是原來的2倍,結點要么在新數組的相對原位置i,要么在i+OldTableSize處,所以其他線程在擴容別的小任務時,不會和當前線程存在位置沖突)。對於擴容時,鏈表同樣先找lastRun然后掛接到新數組上,前面的結點再通過復制的機制掛接到新數組上。

HashMap並發問題:死循環的原因
Hashmap的Resize包含擴容和ReHash兩個步驟,ReHash在並發的情況下可能會形成鏈表環。因為,鏈表采用頭插法,將原數組轉移到新數組時,會從前向后遍歷鏈表結點,頭插法機制恰好使新數組中結點的相對順序和原數組中顛倒過來。在並發的時候,假如原來的結點順序被線程A顛倒了,而被掛起的線程b在恢復執行后,拿擴容前的節點和順序繼續完成第一次循環,而后又遵循A線程擴容后的鏈表順序重新排列鏈表中的順序,即又顛倒了一下順序,最終形成了環。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM