图解JDK1.7中HashMap头插法扩容造成的死循环问题

JDK1.7中HashMap头插法扩容造成的死循环问题

一、背景

HashMap是线程不安全的,在并发使用HashMap时很容易出现一些问题,其中最典型的就是并发情况下扩容之后会发生死循环,导致CPU占用100%。同时,这也是一个高频面试题。本文通过解读HashMap源码并结合实例,来具体分析HashMap扩容发生的死循环问题。

视频学习

大厂面试题:HashMap扩容死循环问题


二、源码解读

下面这段代码是JDK 1.7中HashMap的resize方法,即扩容时调用的代码,作用是创建新的Entry数组newTable,然后调用transfer方法将原来的Entry数组中的节点都转移到newTable中,最后将HashMap的成员变量table指向newTable,所以扩容机制的核心代码在transfer方法中。

    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];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

下面这段代码是JDK1.7中HashMap的transfer方法,作用是遍历原来table中每个位置的链表,并对链表中的每个节点重新进行hash,在新的Entry数组newTable中找到归宿,并插入。

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            //获取链表的头节点e
            while(null != e) {
                //获取要转移的下一个节点next
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //计算要转移的节点在新的Entry数组newTable中的位置
                int i = indexFor(e.hash, newCapacity);
                //使用头插法将要转移的节点插入到newTable原有的单链表中
                e.next = newTable[i];
                //将newTable的hash桶的指针指向要转移的节点
                newTable[i] = e;
               //转移下一个需要转移的节点e
                e = next;
            }
        }
    }

其中最关键的就是其中的 **do while()**循环,这里面就是会发生循环链表的代码。下面再贴一遍代码

do {                // 循环遍历刚才记录下来的链表,把所有键值对都采用头插法插入到新数组对应链表
    Entry<K,V> next = e.next;        // 记录下当前结点的下个结点
    int i = indexFor(e.hash, newCapacity);    // 求出该键值对在新数组的下标,即该键值对应该被插入到新数组第几个链表
    e.next = newTable[i];    // 把结点的next指针指向新数组的第i个链表头结点
    newTable[i] = e;    // 新数组第i个链表的头结点前移,指向当前结点
    e = next;        // 把指向当前结点的指针后移
} while (e != null);

三、图解

现在先走一遍正常扩容的流程,假设有下面这个HashMap, 假设数组大小为2

img

现在需要对它进行扩容,扩容后数组大小为原来的两倍,创建一个大小为4的数组

img

假设a、b两个数扩容后刚好又hash冲突了,即又在同一个链表中,所在下标为3;c在下标为1的链表中。下面开始扩容。

e指针指向了老数组的第1个链表


单线程环境中扩容

执行上面的do while循环,第一轮循环:

img

第二轮循环

img

第三轮也是最后一轮循环,前面已经假设结点 c 将在新数组中的第二个链表

img

至此,老数组中的健值对已全部拷贝到新数组中


多线程环境中扩容

假设在第 二 次循环中的第二步(执行完e.next = newTable[i];)后当前线程的时间片刚好用完了,当前线程被挂起,这时刚好又有一个线程 P2 也来执行扩容操作,它并不会从第二步开始执行,而是重新从第一步开始执行,加入新线程后的扩容图为

img

可以看到,线程2扩容之后的newTable中的单链表形成了一个环,后续执行get操作的时候,会触发死循环,引起CPU的100%问题。

四.总结

通过解读HashMap源码并结合实例可以发现,HashMap扩容导致死循环的主要原因在于扩容过程中使用头插法将oldTable中的单链表中的节点插入到newTable的单链表中,所以newTable中的单链表会倒置oldTable中的单链表。那么在多个线程同时扩容的情况下就可能导致扩容后的HashMap中存在一个有环的单链表,从而导致后续执行get操作的时候,会触发死循环,引起CPU的100%问题。所以一定要避免在并发环境下使用HashMap。

曾经有人把这个问题报给了Sun,不过Sun不认为这是一个bug,因为在HashMap本来就不支持多线程使用,要并发就用ConcurrentHashmap。


参考

HashMap扩容死循环问题解析

深入浅出HashMap扩容死循环问题

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FrozenPenguin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值