從Java1.8原始碼角度剖析執行緒不安全的HashMap
文章目錄
HashMap的底層核心資料結構是什麼?
HashMap<K, V>繼承自AbstractMap<K, V>, 實現了Map介面,Cloneable,Serializable。
核心的儲存結點是一個Node陣列。大小是2的冪,除非是0。
transient Node<K,V>[] table;
其中的Node(Basic hash bin node, used for most entries)實現了Map.Entry介面,
主要包含了hash值,key, value, 和下一個Node的指標(同一slot有多個元素是用鏈式儲存)。
HashMap包含哪些資料結構?
陣列、連結串列、紅黑樹。
雜湊槽(slot)的位置是如果確定的?如何避免雜湊衝突?
i = (n - 1) & hash
hash計算:int hash = hash(key);
hash函式:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //使h的高位參與運算,有助於泊松分佈
}
resize()是如何實現的?
Java1.8中resize函式是無參的。首先計算新的容量大小:
int oldThr = threshold;//The next size value at which to resize (capacity * load factor).
if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
然後迴圈對table陣列中的Node做遷移工作,陣列中沒有資料的位置略過,只對有元素的位置做處理。先將結點儲存在e中,再把該結點置為null,重新找位置存放e
如果該位置(既現在的e)只有一個結點:
if (e.next == null) newTab[e.hash & (newCap - 1)] = e;//還是用hash與陣列長度來計算slot位置
如果該位置存放了多個結點,對原先連結串列的頭尾結點引用,保證有序性(preserve order)。
為什麼執行緒不安全?
從put()方法開始分析,put方法的實質是putValue()方法,第一個引數為hash(key)。
一、如果計算出來的雜湊槽slot位置無元素,則新增結點:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
二、否則,宣告一個結點e (為了簡化先不討論TreeNode)
先比較slot位置的第一個結點雜湊值再比較key值,如果都相等,將e指向該結點:
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//這裡先比較hash值會加快key的比較速度。
e = p;
如果不相等,再依次比較slot位置的後續結點,如果後續結點為空,則新增。
if ((e = p.next) == null)
p.next = newNode(hash, key, value, null);//此處如果多執行緒操作,會丟失結點
如果不為空,再比較key值,如果相同則停止,如果還不同,則再繼續比較下一個。
以上操作的目的,是將e指向key應該儲存的位置。如果該key處已經有舊的value則替換,返回舊的value。
如果新增元素超過了threshold,則進行resize()。從程式碼上看是先增加元素再擴容,而JDK7是先擴容再增加元素。如果在擴容的時候,正好有get操作,則取到的是舊槽的元素。
if (++size > threshold) resize();
下面回答為什麼執行緒不安全的問題。通過以上分析,可以看到put方法用了e來臨時儲存結點,而且用了大量的指標的賦值操作。當多個執行緒併發執行的時候,容易出現指標錯亂,造成結點丟失,或者出現迴圈連結串列。
什麼時候會樹化?
當某個槽內的元素增加到8個時,由連結串列轉為紅黑樹;
當某個槽內的元素減少到6個時,由紅黑樹轉為連結串列。
負載因子loadFactor有什麼用?
預設值是0.75。當元素數量大於 size*loadFactor時進行擴容,這樣既不會浪費過多資源,又可以減少雜湊衝突。
陣列預設大小多少? 最大容量多少?如何初始化?
預設大小:1 << 4;
最大容量:1 << 30;
HashMap的缺點
HashMap的執行緒不安全,put和get操作容易造成死鏈問題,造成CPU佔滿,擴容有可能造成結點丟失。因此在多執行緒環境下使用ConcurrentHashMap, 下一篇文章介紹。