-
HashMap从1.8开始做了修改,修复了1.8之前版本的死循环问题
-
JDK1.8之前版本死循环原因发生在扩容阶段,扩容关键代码如下:
// newTable扩容后的空数组,是HashMap中的共享数组 void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; // 下面这段代码的意思是: // 从OldTable里摘一个元素出来,然后放到NewTable中 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]; // 关建行1 // 此处是发生死循环的关键 newTable[i] = e; e = next; } while (e != null); } } }
分析:for循环是遍历链表,采取的头插方式(后插入的放在开头,指向早插入的),并且是一边遍历一边对共享数组的赋值。关键点在于,后插入的指向新数组中已经插入的,例如:
Thread 1线程执行到 关键行1 然后挂起,Thread2执行完扩容的左右操作,让后Thread1继续执行,此时newTable[i]取到的值为Thread2已经扩容完毕的值,假设扩容完毕的链表结构为:3----》2----》1----》null ,而Thread1此时的节点假设为2,本应该指向1----》null ,结果由于并发被Thread2扩容完毕,结果2指向了3----》2----》1----》null ,结果为 2—》3----》2----》1----》null ,此时2和3行程了闭环。
- 从JDK1.8之后开始,扩容机制发生了变化,关键代码如下:
…此处省略若干行
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;
else if (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;
关建行1 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;
}
关建行2 } while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
分析:从JDK1.8开始修复了死循环问题,不再是头插方式,而是后插入的放在尾部,比如插入顺序是1,2,3 ,则链表结构为1----》2----》3----》null ,并且扩容后不再是一边遍历一边从共享数组中取值,二是组装完毕链表后直接赋值给共享数组。