在JDK1.7及以前中,如果在並發環境中使用HashMap保存數據,有可能會產生死循環的問題,造成cpu的使用率飆升。之所以會發生該問題,實際上就是因為HashMap中的擴容問題。
HashMap的實現實際上是一個數組+鏈表的實現(JDK1.8中當鏈表長度達到一定值會轉化為紅黑樹),當HashMap中保存的值超過閾值時將會進行一次擴容操作,並發環境下可能存在一個線程發現HashMap容量不夠需要擴容,而在這個過程中,另外一個線程也剛好進行擴容操作,這時就有可能造成死循環的問題。擴容操作一般是在調用put(...)方法時進行的,put時會對容量進行檢查。如果在擴容是鏈表中產生一個環形鏈表,那么在使用get(...)獲取數據時將可能產生死循環。
//進行擴容時調用的方法
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];
//將原數組中的元素遷移到擴容后的數組中
//死循環就是在這個方法中產生的
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(null != e) {
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;
}
}
}
在並發環境中,多個線程並不是一起執行的,而是由cpu來調度,任何線程在任意時刻都有可能停下來,當線程停下來時狀態實際上被保存在棧幀中,下一次被cpu分配到執行權時從讀取當前狀態開始繼續執行,對於被cpu掛起的線程在掛起這段時間相當於是"時間暫停"了。明白了這些接下來我們模擬死循環出現的情況,實際上這種情況是很不容易出現的,因為需要的條件較多,但是如果我們線上環境執行頻率很高的話產生這種情況就顯得比較容易了,一旦產生一次那就是災難,除了修改代碼重啟服務器基本沒別的解決方法。
假設當前HashMap中數組長度為1,並且只保存了兩個值(設置的值都是為了產生死循環),如下圖,改圖表示一個長度為1的數組,保存值為1和3的兩個值:

現在假設兩個線程都在進行擴容操作,線程1剛開始,當走到Entry<K,V> next = e.next;時線程掛起,cpu被分配給線程2。
線程2在cpu分配的執行時間中對HashMap操作后變成下圖所示。擴容后,數組長度變為2,由於擴容是重新插入(頭插法)的原因,值得順序變了,現在value=3持有value=1的引用。此時線程2掛起,cpu切換到線程1。之前保存的狀態中e的值為value=1的entry且e包含value=3的值得引用。

線程1繼續之前的操作:
e.next = newTable[i];
newTable[i] = e;
e = next;
經過兩次循環后:

看起來和正常的沒有什么區別,理論上來說,正常情況下由於value=3的entry的next為null此時應該跳出循環,但是問題在於之前的線程2使得value=3持有value=1的引用,這時還會在進行一次循環:
//此時e = value(1)
Entry<K,V> next = e.next; //next=null
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; //e.next=value(3) 死循環了
newTable[i] = e;
e = next;
由上可知此時value(3).next=value(1) 並且value(1).next=value(3),產生了環形鏈表。
如果之后調用get(...)方法,能找到還好,如果查找的值不存在,那么get方法會在環形鏈表處一直循環無法退出,只能重啟服務器了...
