一、扩容过程
我们知道,HashMap在扩容的时候,是通过重新创建一个新的hash表,把原来旧数组中的Entry一个个迁移到新数组的,这个过程的实现方法如下
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;
//遍历第j条链
do {
Entry<K,V> next = e.next;
// 计算在newTable中的位置,原来在同一条链上的元素可能被分配到不同的位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
该方法中最核心的便是链的重新生成,代码如下
Entry<K,V> next = e.next;
// 计算在newTable中的位置,原来在同一条链上的元素可能被分配到不同的位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
举个例子,现在有一条链,里面有四个元素,分别是
经过计算,它们中e1,e2,e4要迁移到newTable[2]中,初始状态为
e指向e1,next指向e2
1.迁移e1
由于一开始newTable[2]里没有元素,所以newTable[2]为null
e.next=newTable[2];将e1连接到了null
newTable[2]=e; 此时新数组中链头放了e1
e=next;指针后移,状态如下
2.迁移e2
e.next=newTable[2];将e2的连接到了e1
newTable[2]=e; 此时新数组中链头放了e2
e=next;指针继续后移,状态如下
3.迁移e3
e3会迁移到另一个桶里,原理跟迁移e1是一样的
4.迁移e4
同理e2
最后迁移完的链为 e4--->e2--->e1
newTable[2]桶里装着e4
小总结:其实就是对着原链表,利用一个桶位进行重链(把新进来的元素连接上链头元素,并占用这个链头)
二、多线程下出现的问题
假设有两个线程,1和2
线程1执行到第一步,也就是这个状态,就被挂起了
然而线程2顺利的完成了该子链的迁移,如下
此时我们发现,线程1的e和next如上,还是指向在e1和e2,然而在内存中整条链的顺序都变了
在这时,线程1又继续工作,按照指令
迁移第1次:
e.next=newTable[2]; 此时的链头已经由线程2改为e4了,所以e1会连上e4
newTable[2]=e;
e=next; 这里e1又指向e2
next=e.next;变为
迁移第2次:
将e2连接上链头e1,e2作为链头,变为
迁移第3次:
将e1连接上链头e2,e1作为链头,e指向next,next指向e的下一个,变为
我们会发现迁移来迁移去都无法跳出这个环形链表,所以会发生扩容错误
以上是我个人的理解,如果读者发现有误还请不吝提醒