先來看一看老版本HashMap擴容代碼:
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():
void transfer(Entry[] newTable) {
//復制一個原數組src,Entry是一個靜態內部類,有K,V,next三個成員變量 Entry[] src = table;
//數組新容量 int newCapacity = newTable.length;: // 從OldTable里摘一個元素出來,然后放到NewTable中 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;//next是當前元素下一個 int i = indexFor(e.hash, newCapacity);//i是元素在新數組的位置 e.next = newTable[i];//此處體現了頭插法,當前元素的下一個是新數組的頭元素 newTable[i] = e;//將原數組元素加入新數組 e = next;//遍歷到原數組某一位置下的一串元素的下一個
} while (e != null);
}
}
}
接下來圖示單線程情況下,do循環內的情況:
初始:當前數組容量為2,有三個元素3、7、5,此處的hash算法是簡化處理(對容量取模)。因此,3、7、5都在數組索引1對應的鏈表上。
擴容新容量為2*2=4。
第一步:當前Entry e對應3,next對應7,新位置i為3,然后將3插入新數組對應位置。
第二步:當前Entry e對應7,next對應5,新位置i為3,然后將新數組對應索引處的元素3添加到7的尾巴后(頭插),然后將7插入新數組對應位置。
第三步:當前Entry e對應5,next對應null,新位置i為1, 然后將5插入新數組對應位置。
接下來圖示多線程情況下死循環場景:初始條件相同。
如果有兩個線程:
線程一執行到 Entry<K,V> next = e.next; 便掛起了,即此時Entry e是3,next是7,3是在7前面的。
線程二執行完成。
此時如下圖所示,線程一的3的next是7,而線程二的7的next是3。(此處是Entry里的next成員變量,在多個線程中相同Entry不沖突)。此時可以看出出現了死循環問題。
如果此時線程一繼續往下執行:
第一步:當前Entry e對應3,next對應7,新位置i為3,然后將3插入新數組對應位置。
第二步:當前Entry e對應7,next對應3(單線程情況下是5),新位置i為3,然后將7插入新數組對應位置。
第三步:當前Entry e對應3,next對應7,此處死循環,永遠不會跳出while循環。
總結歸納:多線程情況下,使用頭插法會導致鏈表節點之間的關系混亂,出現倒排現象,例如原本3->7->5變成7->3,其他線程此時再進行擴容是會出現死循環。
單線程
0
1 3 ->7 ->5
e next
e next
e next=null
0
1 5
2
3 7 -> 3
多
0
1 3 ->7 ->5
e next 線程池一中斷
線程二執行完
0
1 5
2
3 7 -> 3
線程一繼續
出現死循環問題