HashMap的擴容機制:JDK1.7,JDK1.8
參考文章:
以前看HashMap的時候,resize擴容部分沒有仔細看,今天擠出時間,看之。
1. 確定hash桶陣列的索引位置
我們當然希望這個HashMap裡面的元素位置儘量分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用hash演算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,不用遍歷連結串列,大大優化了查詢的效率。HashMap定位陣列索引位置,直接決定了hash方法的離散效能。先看看原始碼的實現(方法一+方法二):
方法一: static final int hash(Object key) { //jdk1.8 & jdk1.7 int h; // h = key.hashCode() 為第一步 取hashCode值 // h ^ (h >>> 16) 為第二步 高位參與運算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 方法二: static int indexFor(int h, int length) { //jdk1.7的原始碼,jdk1.8沒有這個方法,但是實現原理一樣的 return h & (length-1); //第三步 取模運算 }
這裡的Hash演算法本質上就是三步:取key的hashCode值、高位運算、取模運算。
第一步:顯而易見,直接取就是了。
第二步:在JDK1.8的實現中,優化了高位運算的演算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼做可以在陣列table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。
第三步:不是 直接的取模運算,由於length為2的冪次,所以可以使用“與”運算 h & (length-1)代替取模運算。這樣速度更快。
2. resize()方法
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; boolean oldAltHashing = useAltHashing; useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean rehash = oldAltHashing ^ useAltHashing; transfer(newTable, rehash); //transfer函式的呼叫 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
其實就是新建一個 newCapacity大小的陣列,然後呼叫transfer()方法將元素複製進去。
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); //重新獲取hashcode
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
來看看JDK1.7的trantransfer方法吧,可以看到其中是一些指標的操作,首先將 當前節點e的next節點儲存下來,然後找到在新陣列中的 下標 index, 使用頭插法插入節點, 然後 接著處理 next 節點。
這裡有兩個重要的地方:
1. JDK1.8 當中 是如何找到新的陣列中的下標的。
圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容後key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的雜湊與高位運算結果。
可以看到, 此key 要麼在原先的index上, 要麼在 index+old_length 上,old_length為擴容前陣列的長度。
這樣做的好處:增加隨機性了,hashmap效能更好了。 效率更高了,不需要重新計算了。
2. 在JDK1.7中這樣使用頭插法在多執行緒的場景下是會出問題的,會形成一個環。
具體就是 執行緒a 一個節點插入完成後,還沒有執行 e = next ,執行緒 b 執行 next = e.next , 執行緒a此時執行 e = next,此時就變成剛剛插入節點的下一個節點了,然後呼叫 e.next = newTable[i] , 這樣第二個執行緒就會變成一個死迴圈了。
3.JDK1.8中的改變
JDK1.8中不採用頭插法了,也就不存在出現環的情況了,具體可以見下圖。