HashMap作為老生常談的問題,備受面試官的青睞,甚至成為了面試必問的問題。由於大量的針對HashMap的解析橫空出世,面試官對HashMap的要求越來越高,就像面試官對JVM掌握要求越來越高一樣,今天我們來研究下HashMap的連結串列環化的問題,你知道其中的原理嘛?關注公眾號“程式設計師清辭”,獲取更多內容
在JDK1.7版本下,有個執行緒安全的問題,經常會被問到,很多求職者可能還在對比Hashtable執行緒安全性,其實面試官想得到的連結串列成環造成執行緒安全的問題,而這個問題在JDK1.8中已經得到了解決,但至於出現這樣問題的原因,我翻看了很多帖子,大家剖析的很透徹,但是很難理解,今天結合自己的研究利用一篇帖子來闡述其中的奧祕。
JDK1.7擴容原始碼解析
首先我來了解下HashMap中經典的擴容程式碼,回顧下擴容的過程
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//......
// 擴容方法
void resize(int newCapacity) {
// 1、建立臨時變數,將HashMap陣列資料賦值給新陣列作臨時儲存
Entry[] oldTable = table;
// 2、判斷老陣列長度是否超過了允許的最大長度,最大長度為 1 << 30
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 3、建立新的Entry陣列,並擴容
Entry[] newTable = new Entry[newCapacity];
// 4、擴容賦值,即將老陣列中的資料賦值到新陣列中
// initHashSeedAsNeeded(newCapacity) 得到的是一個hash的隨機值(雜湊種子),
//在計算雜湊碼時會用到這個種子,作用是減少雜湊碰撞
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 6、擴容後賦值
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// newTable : 表示新陣列,即擴容後建立的新陣列
// rehash : 是否需要重新計算雜湊值
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 5、將老map中的資料賦值到新map中(陣列和連結串列複製遷移)
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);
}
// 計算Entry元素在Entry[]陣列中的位置
int i = indexFor(e.hash, newCapacity);
// 連結串列頭插法賦值過程
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
//......
}
- 建立臨時變數,將HashMap陣列資料賦值給新陣列作臨時儲存
- 判斷老陣列長度是否超過了允許的最大長度,最大長度為 1 << 30
- 建立新的Entry陣列,並擴容
- 擴容賦值,即將老陣列中的資料賦值到新陣列中
- 將老map中的資料賦值到新map中(陣列和連結串列複製遷移)
- 擴容後賦值
連結串列遷移過程
以下三行程式碼描述了連結串列頭插的整個過程,下面來剖析下這個過程:
e.next = newTable[i];
newTable[i] = e;
e = next;
關注公眾號“程式設計師清辭”,獲取更多內容
假設HashMap的儲存狀態如下:
e為陣列位置的元素,e1、e2為e下形成的連結串列,h為將要賦值的位置,箭頭代表連結串列指向
e.next = newTable[i]
對oldTable進行遍歷的過程中,取出元素e,假設先取出圖中的元素e,在執行這行程式碼時,相當於斷開x位置e與e1的連結串列關係,並與newTable[i]建立連結串列關係,此時newTable[i]位置為null
newTable[i] = e
此時將oldTable中的e複製到newTable中的i位置,同時連結串列e指向null
問題:那oldTable中e1和e2形成的連結串列怎麼辦?
其實在之前的程式碼中已經闡述了,詳情如下:
while(null != e) {
// 這裡已經將e.next儲存為一個臨時變數,也就是e1和e2形成的連結串列
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 :hash(e.key);
}
......
}
e = next;
將next的值賦值給e,這行程式碼對上述的連結串列沒有實質的影響,並且這已經是while迴圈的最後一行程式碼了,這行程式碼的目的是為下一次while遍歷過程能從e1元素開始,而不是e,因為此時需要的遍歷的e已經變成了e1。
通過這次資料遷移可能沒有得到比較有參考意義的分析,所以我們需要再進行一次遍歷分析,而這次遍歷分析從e1開始。這裡就不詳細闡述,直接上圖。
e.next = newTable[i]
newTable[i] = e
最終效果
以上就是整個資料遷移的過程,通過連結串列例項大家發現HashMap利用頭插法完成遷移的過程,下面進入重點,連結串列成環
併發操作連結串列成環
產生基本條件
- 多執行緒環境併發操作
- HashMap擴容時候發生
問題解析
在多執行緒環境下,a,b兩個執行緒同時操作這個HashMap,由於HashMap是執行緒不安全的,假如執行緒a已經完成以上全過程,也就是下圖
程式碼執行到如下位置,還沒有完全的出棧
此時執行緒b同時也在遍歷這條連結串列,同時程式碼執行到while迴圈位置
這時執行緒b已經重新獲取e資料時,由於a執行緒的操作還沒有將資料同步到主記憶體,導致出現如下情況:
問題總結
- 插入的時候和平時我們追加到尾部的思路是不一致的,是連結串列的頭結點開始迴圈插入,導致插入的順序和原來連結串列的順序相反的。
- table 是共享的,table 裡面的元素也是共享的,while 迴圈都直接修改 table 裡面的元素的 next 指向,導致指向混亂。