HashMap jdk7死循环场景

1.前言

在java编程中,用到的集合容器非常多,hashmap就是其中一种。

2.数据结构

hashmap底层的数据结构在jdk8之前是数组加链表,在jdk8改成了数组、链表、红黑树。

3.死锁

jdk7版本的hashmap在多线程的环境下可能回造成cpu100%的情况,接下来分析下这个场景。

  • 分析

先贴一段jdk7的扩容代码

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry e : table) {
		while(null != e) {
			Entry next = e.next;
			if (rehash) {
				e.hash = null == e.key ? 0 : hash(e.key);
			}
			int i = indexFor(e.hash, newCapacity);
			e.next = newTable[i];
			newTable[i] = e;
			e = next;
		}
	}
}

大致解释下上面的代码:

首先这是扩容是触发的代码

第二行是获取新的数组的长度

接下来开始遍历扩容前的数组

数组下标里面对应的每个节点都有可能是链表,因此通过while在遍历链表

接下来第五行进来 代表e不为空,把e的下一个节点赋值给临时节点

第六行和第七行是重新计算下hash值

第9行是根据hash值和新数组长度算出数组下标

接下来将存在该数组下标的节点赋值给e.next

然后把e给当前数组下标中

然后如果next不为空的情况下就继续遍历下一个节点

这样理下来就清楚多了,hashmap链表扩容实际上是将原来的链表与新的链表调换了位置,加在头节点。

  • 单线程

接下来开始一步一步分析这段代码存在的问题,先来看看单线程的情况。

假设数组长度为2,table[1]里面有三个节点分别为n1->n2->n3,

接下来走一遍代码

先遍历数组,再遍历链表,那么重点从第五行开始,当前的e=n1,e.next=n2,那么此时next=n2,到第10行newTable[i]=null,因此e.next=null,再把现在的e也就是n1赋值给newTable[i],那么newTable[i]=n1。第十二行e=next,也就是e=n2,进入下一次循环。最终得到的结果是n3->n2->n1。

再单线程环境下是没问题的。接下来看多线程。

  • 多线程

现在有t1、t2两个线程,table还是长度为2的数组,table[1]还是n1->n2->n3。

接下来,t1线程把n1成功的放入到新数组中,那么此时的变量关系为
newTable[i]=n1,e=n2,e.next=n3。

那么当t1线程再次走到第十行的时候此时table[i]=n1,e=n2,next=n3,那么t2线程还是进行扩容,走到第五行,此时把next变为e.next,这个时候t1线程已经把n1赋值给e.next,因此t2线程就会在第五行把n1又赋值给next,那么下次遍历又是遍历n1节点,就形成了闭环。死循环就是这么来的。

为什么会形成这个问题呢?是因为在jdk7中是将相同hash的节点新增到链表的头部。

4.jdk8的改进

在jdk8中改进了这一点,将相同hash的节点新增到链表的尾部,这样就不会形成闭环。
由于每次放在链表尾部,会导致需要遍历链表,因此jdk8中当链表的长度大于等于8的时候就会转换成红黑树。这也能对链表进行了优化。

5.总结

最后,多线程的场景下还是用ConcurrentHashMap,单线程的场景下用HashMap。

你可能感兴趣的:(hashmap)