今天在看《分佈式java應用》這本書的時候看到作者提到HashMap在多線程併發的環境下有可能出現死循環,導致cpu100%的現象,看了下源碼結合網上的分析說明下這種可能性。可能出現問題的地方是在擴容的時候
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);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
這個方法本身沒有問題,問題出在transfer(newTable);這個方法是用來移動oldTable裏的數據到newTable裏。
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
//(1)
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
//(2)
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
//(3)
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
下面分析可能出現的情況,假設原來oldTable裏存放a1,a2的hash值是一樣的,那麼entry鏈表順序是:
P1:oldTable[i]->a1->a2->null
P2:oldTable[i]->a1->a2->null
線程P1運行到(1)下面這行時,e=a1(a1.next=a2),繼續運行到(2)下面時,next=a2。這個時候切換到線程P2,線程P2執行完這個鏈表的循環。如果恰a1,a2在新的table中的hash值又是一樣的,那麼此時的鏈表順序是:
主存:newTable[i]->a2->a1->null
注意這個時候,a1,a2連接順序已經反了。現在cpu重新切回P1,在(3)這行以後:e.next = newTable[i];即:a1.next=newTable[i];
newTable[i]=a1;
e=a2;
開始第二次while循環(e=a2,next=a1):
a2.next=newTable[i];//也就是a2.next=a1
newTable[i]=a2
e=a1
開始第三次while循環(e=a1,next=null)
a1.next=newTable[i];//也就是a1.next=a2
這個時候a1.next=a2,a2.next=a1,形成迴環了,這樣就造成了死循環,在get操作的時候next永遠不爲null,造成死循環。
可以看到很偶然的情況下會出現死循環,不過一旦出現後果是非常嚴重的,多線程的環境還是應該用ConcurrentHashMap。