在JDK1.7及以前的版本,如果在并发环境中使用HashMap保存数据,有可能会产生死循环的问题,造成cpu的使用率飙升。产生这个问题是因为JDK1.7及以前的版本中,HashMap扩容采用的是头插入,1.8做的改进是采用尾插法,所以不会造成死循环的问题。
首先,来看1.7扩容的代码:

 //进行扩容时方法
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];              //---------111
        //多线程情况下,上面创建好新的数组,死循环就是在下面方法中产生的
        transfer(newTable, initHashSeedAsNeeded(newCapacity));  //---------222
        table = newTable;                                       //---------333
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 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;                      //----------444
                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;
            }
        }
    }

一开始的数据结构如图:
JDK1.7的HashMap链表死循环分析
假设两个线程,第一个线程执行到注释111处,cpu时间执行完了,轮到第二个线程执行,第二个线程执行完注释111处,进入到注释222的方法,执行到444处(这个地方很重要),这时变量e指向节点a,变量next指向节点b,第二个线程的cpu执行完了,这时候两个线程都创建了新的哈希表,如图;
JDK1.7的HashMap链表死循环分析
又轮到第一个线程执行,假设a,b,c三个节点刚好映射到7这个位置,
先移动a节点,如图;
JDK1.7的HashMap链表死循环分析
再移动节点b,如图;
JDK1.7的HashMap链表死循环分析
最后移动节点c,如图;结合三张图片可以看出来,头插法,就是从链表的头部插入

JDK1.7的HashMap链表死循环分析
这个时候线程1的时间片用完,也就是注释222的方法已经执行完毕,但是还没执行注释333,也就是内部的table还没有设置成新的newTable,这时候线程2开始执行,这时内部的引用关系如下:
JDK1.7的HashMap链表死循环分析

再贴一下tranfer方法的代码,我们刚才说线程2执行到注释444处,这时变量e指向节点a,变量next指向节点b

 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;                      //----------444
                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;
            }
        }

继续执行,7会指向节点a;然后b成了变量e,
JDK1.7的HashMap链表死循环分析
因为e不是null,则继续执行循环体,7会指向节点b,b指向a,因为上一张可以看出节点本来就指向a,所以变化如图,此时a就变成了节点e。
JDK1.7的HashMap链表死循环分析
下面就是链表成环的关键;
e.next = newTable[i]; newTable[i]也就是7的位置,指向的是节点b,所以会把节点a的next指向b,而从上面的图可以看到,节点b的next是指向节点a的,这样就构成了死循环。
newTable[i] = e;将7的位置指向的是节点a。此时结构如图:
JDK1.7的HashMap链表死循环分析
另外,可以看出,如果线程2执行到了注释333时,把newTable设置成到内部的table,节点c的数据就会丢了。
明天会对1.8的源码进行分析,包括put,resize,以及为什么容量是2的倍数,以及为什么链表有8个节点转化成红黑树,谈谈自己的见解。

之前自己也没懂为什么1.7hashmap为造成死循环,看了下面这篇文章才看懂,推荐一下,自己懒得画图,也借用了文章的图。
https://www.jianshu.com/p/1e9cf0ac07f4

相关文章: