HashMap原始碼探究(死鎖/擴容)【JDK1.7】【JDK1.8】
先說HashMap最重要的一點:缺點
HashMap的缺點我們大都聽說過,其在高併發的情況下表現較差,會出現一些奇奇怪怪的問題,比如使CPU使用率提高到100%(此處打個小差,因為前幾天,我的伺服器莫名其妙CPU佔用率也達到了100%,我還以為是跑了哪個專案寫的有問題了,後來查了一下所有程序才發現有個ddgs的一直在高佔用,經過研究發現,這是一個新型的挖礦病毒,中毒原因是我之前練習redis的時候忘了設定密碼o(╥﹏╥)o),那麼它為什麼會出現這個原因呢?其實這個高併發下的問題,也和HashMap一個長久以來的缺點相掛鉤,沒錯,就是HashMap 的擴容機制。
為什麼說HashMap的擴容機制是長久以來的缺點,我們可以簡單看一下其原始碼,可知:其初始化長度為16,擴容因子為0.75(即當內容達到百分之七十五的時候會擴容為當前的二倍),那麼問題其實就在於HashMap是怎樣進行擴容的。
它在擴容的時候,會先生成一個新的HashMap,然後把原HashMap裡的資料一個一個的複製到新的HashMap裡,那麼就很輕易的知道了,當我們的資料量過大的時候,我們的HashMap會進行多次擴容,那麼就會相對來說很消耗我們的資源,解決這個缺點的辦法也很簡單,先大致預估一下我們需要在這個HashMap裡存放多少資料,然後在初始化它的時候給它先把預設長度給設定了,這樣就避免了多次擴容多次複製。
介紹完了擴容機制,那麼其在高併發下那個100%的問題是怎麼來的呢,不難想出,上述步驟中,最有可能出現問題的,就是複製的那一步。
//擴容的方法 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]; //在此進行我們所說的複製的那一步 傳入的引數為新HashMap, 初始化hash掩碼(此處不太懂也沒事) transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } //最重要的方法 void transfer(Entry[] newTable, boolean rehash) { //先儲存新陣列長度 int newCapacity = newTable.length; //然後依次遍歷我們的老陣列,由此我們也可以知道,我們的複製是從老表的頭部開始的 for (Entry<K,V> e : table) { //這個while迴圈是我們問題的關鍵 while(null != e) { /*當我們遍歷到一個不為空的老資料的時候,我們假定這個老資料在橫向(我們知 道HashMap是由陣列和連結串列構成的,那麼假設一個二維空間裡HashMap縱向是陣列結構,橫向是連結串列結構)掛載這有下一個節點, 那麼我們現在想移動這個老資料,必須得保證我們下邊節點的資料不丟失,所以我們建立一個next先去"指向"它*/ 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; } } }
在這個transfer方法中,當我們看到
Entry<K,V> next = e.next;
的時候,我們就應該有這樣一個擔憂,在高併發的情況下,這一步會不會影響我們的擴容,答案是肯定的……
但是說這是不是一個BUG ? 並不
sun公司的負責人表達的意思是:我們設計HashMap本來的作用就不應該是應對高併發情況的,在高併發的情況下,我們有另外一個更好用的ConcurrentHashMap 去應對。
HashMap的put方法注意點
我們通常會簡單認為HashMap的put方法的Key只進行一次hash運算,但事實上,HashMap的put實現是在計算key的hash之上,又進行了一次自己規定的位運算,以JDK1.8中原始碼為例(版本有差距,不過也都是進行了二次位運算)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h; //^異或運算 >>>為帶符號右移
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}