HashMap 死循环问题,是面试中常见的面试题,其实这里主要是为了考察求职者的基础数据结构——链表在并发环境下的问题。
一、头插法
jdk1.8以前的扩容操作,(截取了1.6 的源码)
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.next = newTable[i];
// 指针下移
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
头插法,按说没什么问题,但是在并发环境下,多个线程同时扩容时就会出现问题:
二、尾插法
1.8 改成了尾插法:
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;
}
三、总结
1. 死循环的问题根本原因是多个线程操作(准确地说是修改)同一条链表,导致链表循环,其他线程读取的时候就会发生死循环。
2. 虽然 1.8 扩容时链表改成了尾插法,但是仍然无法保证多线程数据安全,毕竟 HashMap 本身就不是线程安全的。