今天開發環境壓測的時候出現cpu用滿了情況,看線程堆棧,一堆線程都停留在org.apache.commons.collections4.map.AbstractHashedMap.put(AbstractHashedMap.java:285),查看google源代碼
public Object put(Object key, Object value) {
key = convertKey(key);
int hashCode = hash(key);
int index = hashIndex(hashCode, data.length);
HashEntry entry = data[index];
while (entry != null) {
if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) {
Object oldValue = entry.getValue();
updateEntry(entry, value);
return oldValue;
}
entry = entry.next;
}
addMapping(index, hashCode, key, value);
return null;
}
方法是非線程安全的,而addMapping方法會觸發ensureCapacity擴容,而並發擴容就會容易導致死循環, 具體原因參考文章https://www.cnblogs.com/dongguacai/p/5599100.html
解決方法:
1、使用ConcurrentHashMap。
2、使用Collections.synchronizedMap(Mao<K,V> m)方法把HashMap變成一個線程安全的Map。
我copy了部分文章內容
正常的擴容過程
我們先來看下單線程情況下,正常的rehash過程
1、假設我們的hash算法是簡單的key mod一下表的大小(即數組的長度)。
2、最上面是old hash表,其中HASH表的size=2,所以key=3,5,7在mod 2 以后都沖突在table[1]這個位置上了。
3、接下來HASH表擴容,resize=4,然后所有的<key,value>重新進行散列分布,過程如下:

在單線程情況下,一切看起來都很美妙,擴容過程也相當順利。接下來看下並發情況下的擴容。
並發情況下的擴容
1、首先假設我們有兩個線程,分別用紅色和藍色標注了。
2、擴容部分的源代碼:
1 void transfer(Entry[] newTable) { 2 Entry[] src = table; 3 int newCapacity = newTable.length; 4 for (int j = 0; j < src.length; j++) { 5 Entry<K,V> e = src[j]; 6 if (e != null) { 7 src[j] = null; 8 do { 9 Entry<K,V> next = e.next; 10 int i = indexFor(e.hash, newCapacity); 11 e.next = newTable[i]; 12 newTable[i] = e; 13 e = next; 14 } while (e != null); 15 } 16 } 17 }
3、如果在線程一執行到第9行代碼就被CPU調度掛起,去執行線程2,且線程2把上面代碼都執行完畢。我們來看看這個時候的狀態:

4、接着CPU切換到線程一上來,執行8-14行代碼,首先安置3這個Entry:

這里需要注意的是:線程二已經完成執行完成,現在table里面所有的Entry都是最新的,就是說7的next是3,3的next是null;現在第一次循環已經結束,3已經安置妥當。看看接下來會發生什么事情:
1、e=next=7;
2、e!=null,循環繼續
3、next=e.next=3
4、e.next 7的next指向3
5、放置7這個Entry,現在如圖所示:

放置7之后,接着運行代碼:
1、e=next=3;
2、判斷不為空,繼續循環
3、next= e.next 這里也就是3的next 為null
4、e.next=7,就3的next指向7.
5、放置3這個Entry,此時的狀態如圖:

這個時候其實就出現了死循環了,3移動節點頭的位置,指向7這個Entry;在這之前7的next同時也指向了3這個Entry。
代碼接着往下執行,e=next=null,此時條件判斷會終止循環。這次擴容結束了。但是后續如果有查詢(無論是查詢的迭代還是擴容),都會hang死在table【3】這個位置上。現在回過來看文章開頭的那個Demo,就是掛死在擴容階段的transfer這個方法上面。
出現上面這種情況絕不是我要在測試環境弄一批數據專門為了演示這種問題。我們仔細思考一下就會得出這樣一個結論:如果擴容前相鄰的兩個Entry在擴容后還是分配到相同的table位置上,就會出現死循環的BUG。在復雜的生產環境中,這種情況盡管不常見,但是可能會碰到。

