JDK 1.7 HashMap 死循环问题
JDK 1.7 中 HashMap 的链表采用头插法,在多线程同时扩容时可能产生死循环。
扩容代码:
// JDK 1.7
void transfer(Entry[] newTable) {
// 原数组
Entry[] src = table;
int newCapacity = newTable.length;
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;
// 原数组的链表在新数组中的位置
int i = indexFor(e.hash, newCapacity);
// 头插法,e 的后继节点更新为头节点
e.next = newTable[i];
// 头节点更新成 e
newTable[i] = e;
// 继续下一个节点
e = next;
} while (e != null);
}
}
}
死循环发生步骤:
- 线程 A 和线程 B 同时对同一个链表(A -> B -> C)进行扩容;
- 线程 A 暂停在
e.next = newTable[i]
,此时 e 是 A,e.next 是 B; - 线程 B 完成扩容,链表变成 C -> B -> A;
- 线程 A 恢复执行后
-
-
- 将链表头节点置为 A(线程 A 是新的空数组 newTable),A.next = null(newTable[i] == null);
- 在线程 A 暂停前记录了 next == B,则 e 更新为 B 继续循环;
- 将链表头节点置为 B,B.next = A(核心问题在这,这个是因为线程 B 导致的,如果没有线程 B,B 的后继节点应该是 C),e 更新为 A 继续循环;
- 将链表头节点置为 A,此时 A.next = null,将 A.next 置为链表头节点 B,而上一步 B.next = A,产生死循环;
- 上一步中 next = null,退出循环(可以看出除了死循环外,还会丢失其他节点);
-
JDK 1.8 后改为尾插法,解决了死循环问题(尾插法中节点顺序是不变的) 。