jdk1.7 HashMap中的"致命錯誤":循環鏈表
jdk1.7 HashMap結構圖
jdk1.7是數組+鏈表的結構
jdk1.7版本中主要存在兩個問題
-
頭插法會造成循環鏈表的情況
-
鏈表過長,會導致查詢效率下降
jdk1.8版本針對jdk1.8進行優化
- 使用尾插法,消除出現循環鏈表的情況
- 鏈表過長後,轉化爲紅黑樹,提高查詢效率
具體可以參考我的另一篇博客你真的懂大廠面試題:HashMap嗎?
循環鏈表的產生
多線程同時put
時,如果同時調用了resize
操作,可能會導致循環鏈表產生,進而使得後面get的時候,會死循環。下面詳細闡述循環鏈表如何形成的。
resize函數
數組擴容函數,主要的功能就是創建擴容後的新數組,並且將調用transfer
函數將舊數組中的元素遷移到新的數組
void resize(int newCapacity)
{
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
//創建一個新的Hash Table
Entry[] newTable = new Entry[newCapacity];
//將Old Hash Table上的數據遷移到New Hash Table上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
transfer函數
transfer邏輯其實也簡單,遍歷舊數組,將舊數組元素通過頭插法的方式,遷移到新數組的對應位置問題出就出在頭插法。
void transfer(Entry[] newTable)
{
//src舊數組
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);//由於是鏈表,所以有個循環過程
}
}
}
static int indexFor(int h, int length){
return h&(length-1);
}
下面舉個實際例子
//下面詳細解釋需要用到這部分代碼,所以先標號,將一下代碼分爲五個步驟
do {
1、Entry<K,V> next = e.next;
2、int i = indexFor(e.hash, newCapacity);
3、e.next = newTab[i];
4、newTable[i] = e;
5、e= next;
} while(e != null)
-
開始容量設爲2,加載閾值爲
-
線程和線程同時插入元素,由於閾值爲1,所以都需要調用
resize
函數,進行擴容操作 -
線程先阻塞於代碼
Entry<K,V> next = e.next;
,之後線程執行完擴容操作 -
之後線程被喚醒,繼續執行,完成一次循環後
開始, , 執行下面代碼後,,
2、int i = indexFor(e.hash, newCapacity); 3、e.next = newTab[i]; 4、newTable[i] = e; 5、e= next; 1、Entry<K,V> next = e.next;
-
線程 執行第二次循環後
開始, , 執行以下代碼後, $e\rightarrow3 $,
2、int i = indexFor(e.hash, newCapacity); 3、e.next = newTab[i]; 4、newTable[i] = e; 5、e= next; 1、Entry<K,V> next = e.next;
-
線程T1執行第三次循環後,形成死循環
開始, , 執行以下代碼後,
2、int i = indexFor(e.hash, newCapacity); 3、e.next = newTab[i]; 4、newTable[i] = e; 5、e= next; 1、Entry<K,V> next = e.next;