1.7中HashMap的扩容过程是这样的:
- 取当前table的2倍作为新table的大小
- 根据算出的新table的大小new出一个新的Entry数组来,名为newTable
- 轮询原table的每一个位置,将每个位置上连接的Entry,算出在新table上的位置,并以链表形式连接(在插入新table的时候是头插法)
- 原table上的所有Entry全部轮询完毕之后,意味着原table上面的所有Entry已经移到了新的table上,HashMap中的table指向newTable
实例
假如现在hashmap中有三个元素,Hash表的size=2,key=3,7,5,在mod 2以后都冲突在table[1]这个位置上了,如下:
扩容的核心代码如下:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for(Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if(rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算在新table中的位置
int i = indexFor(e.hash, newCapacity);
//一下三行就实现了头插法
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
先看看正常的单线程扩容:
对于table[1]中的链表来说,进入while循环,此时e=key(3),那么next=key(7),经过计算重新定位e=key(3)在新表中的位置(3%4=3),并把e=key(3)挂在newTable[3]的位置
第一次循环完成后的样子:
这样循环下去,将table[1]中的链表循环完成后,于是HashMap就完成了扩容:
以上就是正常的单线程扩容过程,是没问题的。
下面看并发下的扩容
初始的map还是:
假设现在有两个线程并发操作,都进入了扩容操作
以颜色区分两个线程
假设线程1执行到Entry<K,V> next = e.next;时被操作系统调度挂起了,而线程2执行完成了扩容操作
于是在线程1、2各自开来,就是这个样子的:
线程1刚刚开始扩容,第一次循环还没有完成,线程2已经完成了扩容
接下来,假如线程2被调度回来继续执行:
注意:最需要关注的是这三行代码:
e.next = newTable[i];
newTable[i] = e;
e = next;
1. 因为线程1里面,e还是指向key(3),next执行key(7)
执行e.next = newTable[i];和newTable[i] = e;
因为线程1的newTable里面还没有元素,所以e.next肯定是null
执行完后线程1里面是这个样子的
2. 执行e=next;
执行后e指向key(7),next也指向key(7)
左边是线程1
3. 这一轮循环完了,开始新一轮的循环
左边是线程1
因为两个线程操作同一个数据,所以执行Entry<K,V> next = e.next 之后,next指向了key(3)
4. 这一步执行完后,线程1里面通过将key(7)通过头插法插到key(3)的前面
左边是线程1
执行完后如下:
5. 执行 e=next; 执行之后e=key(3)
6. 本轮执行完成,开始新一轮的循环
执行Entry<K,V> next = e.next
这一步执行完后如下图,e指向了key(3)
7. 接下来执行
e.next = newTable[i];
newTable[i] = e;
执行完后,线程1的table变成了如下图所示,会将key(3)通过头插法插到key(7)的前面
因为key(7)的后继节点还是key(3),又将key(3)的后继节点设置成了key(7),所以此时就产生了环形链表
第7步执行完后线程1的扩容过程也执行完了,因为执行了e=next后,e变成了null,线程1会退出循环,然后HashMap的table就会被设置成线程1的newTable
然后,当从map里get一些不存在的值的时候(比如get(11)、get(15)之类的)就会进入死循环,CPU就会暴涨到100%。
总结
HashMap之所以在并发下的扩容造成死循环,是因为,多个线程并发执行时,因为一个线程先完成了扩容,将原Map的链表重新散列到自己的表中,并且链表变成了倒序,后一个线程再扩容时,又进行自己的散列,再次将倒序链表变为正序,于是就会形成一个环形链表,个get表中不存在的元素时,造成死循环。