CPU 飆高?可能是 HashMap 在 “搞事”
前言
在Java開發中,HashMap作為最常用的集合類之一,以其高效的查找和插入性能被廣泛應用。然而在多線程環境下使用時可能會發生死循環場景下,導致CPU負載率飆升至100%,引發系統卡頓或崩潰。
哈希表的基本結構
HashMap 的底層數組稱為桶(Bucket),每個桶存儲哈希沖突的元素。當元素數量超過閾值(容量 × 負載因子)時,HashMap會觸發擴容(resize)操作,將數組容量翻倍并重新分配元素。在JDK1.8中,當鏈表長度超過8時,會自動轉換為紅黑樹以優化查詢性能;當長度減少到6時,又會轉回鏈表節省空間。
線程不安全的根源
HashMap設計之初就不是線程安全的集合類,主要體現在以下場景:
- 多線程并發修改時,可能導致鏈表形成環形結構(JDK1.7及之前)
- 擴容過程中元素遷移不當,引發死循環
- 哈希值計算沖突導致鏈表過長,查詢效率退化至O(n)
導致 CPU 負載率高的具體原因分析
HashMap引發CPU負載過高的問題,本質上是線程安全問題導致的無限循環。
排查過程:
- 通過jstack命令獲取線程快照,發現多個線程卡在HashMap.get()方法
- 分析堆棧信息,確認線程在遍歷鏈表時陷入無限循環
- 檢查代碼發現使用了HashMap存儲會話信息,且存在多線程并發修改
擴容死循環(JDK1.7 及之前)
在JDK1.7中,HashMap的擴容函數(transfer)采用頭插法遷移元素。當兩個線程同時觸發擴容時,可能導致鏈表節點引用形成環形結構。此后對該鏈表的操作會陷入無限循環,持續占用CPU資源。
// JDK1.7 transfer方法關鍵代碼
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; // 線程1在此處掛起
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;
}
}
}
圖片
當線程1獲取next節點后被掛起,線程2完成擴容并修改了節點引用關系,線程1恢復后會基于舊的引用繼續處理,最終形成環形鏈表。每次get操作都會遍歷這個環形鏈表,導致CPU使用率飆升。
之所以選擇使用頭插法,是因為JDK的開發者認為,后插入的數據被使用到的概率更高,更容易成為熱點數據,而通過頭插法把它們放在隊列頭部,就可以使查詢效率更高。
如何解決
前面提到,之所以會發生這個死循環問題,是因為在JDK 1.8之前的版本中,HashMap是采用頭插法進行擴容的,這個問題其實在JDK 1.8中已經被修復了,改用尾插法。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
elseif ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
elseif (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
elseif (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}線程安全替代方案
- ConcurrentHashMap:JDK1.7采用分段鎖機制,JDK1.8基于CAS操作和synchronized實現,支持高并發讀寫,性能遠超Hashtable
- Collections.synchronizedMap():通過包裝HashMap實現線程安全,性能較差但兼容性好
- ConcurrentSkipListMap:適用于需要排序的場景,并發性能優異




























