先来看一看老版本HashMap扩容代码:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; ...... //创建一个新的Hash Table Entry[] newTable = new Entry[newCapacity]; //将Old Hash Table上的数据迁移到New Hash Table上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
其中,重点在于transfer():
void transfer(Entry[] newTable) {
//复制一个原数组src,Entry是一个静态内部类,有K,V,next三个成员变量 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;//next是当前元素下一个 int i = indexFor(e.hash, newCapacity);//i是元素在新数组的位置 e.next = newTable[i];//此处体现了头插法,当前元素的下一个是新数组的头元素 newTable[i] = e;//将原数组元素加入新数组 e = next;//遍历到原数组某一位置下的一串元素的下一个
} while (e != null);
}
}
}
接下来图示单线程情况下,do循环内的情况:
初始:当前数组容量为2,有三个元素3、7、5,此处的hash算法是简化处理(对容量取模)。因此,3、7、5都在数组索引1对应的链表上。
扩容新容量为2*2=4。
第一步:当前Entry e对应3,next对应7,新位置i为3,然后将3插入新数组对应位置。
第二步:当前Entry e对应7,next对应5,新位置i为3,然后将新数组对应索引处的元素3添加到7的尾巴后(头插),然后将7插入新数组对应位置。
第三步:当前Entry e对应5,next对应null,新位置i为1, 然后将5插入新数组对应位置。
接下来图示多线程情况下死循环场景:初始条件相同。
如果有两个线程:
线程一执行到 Entry<K,V> next = e.next; 便挂起了,即此时Entry e是3,next是7,3是在7前面的。
线程二执行完成。
此时如下图所示,线程一的3的next是7,而线程二的7的next是3。(此处是Entry里的next成员变量,在多个线程中相同Entry不冲突)。此时可以看出出现了死循环问题。
如果此时线程一继续往下执行:
第一步:当前Entry e对应3,next对应7,新位置i为3,然后将3插入新数组对应位置。
第二步:当前Entry e对应7,next对应3(单线程情况下是5),新位置i为3,然后将7插入新数组对应位置。
第三步:当前Entry e对应3,next对应7,此处死循环,永远不会跳出while循环。
总结归纳:多线程情况下,使用头插法会导致链表节点之间的关系混乱,出现倒排现象,例如原本3->7->5变成7->3,其他线程此时再进行扩容是会出现死循环。
单线程
0
1 3 ->7 ->5
e next
e next
e next=null
0
1 5
2
3 7 -> 3
多
0
1 3 ->7 ->5
e next 线程池一中断
线程二执行完
0
1 5
2
3 7 -> 3
线程一继续
出现死循环问题