• HashMap扩容死循环问题解析


    一、问题和背景

    昨天面试腾讯被问到了HashMap为什么线程不安全,多线程下会有哪些线程不安全的情况,记忆中隐约记得有个扩容链表成环的问题,但是问到为什么,怎么解决的,JDK1.8对这个问题有做出相关优化吗,gg了,不会。为自己点了一首凉凉。

    二、源码解读

    今天特意上网搜了一下答案,看到两篇博客觉得写得很有道理,深入浅出HashMap扩容死循环问题 和 JDK1.7和JDK1.8中HashMap为什么是线程不安全的?下面记录了一下学习过程和自己的理解。
    当插入一个新的键值对时,会先根据 key 对 HashMap底层数组长度取模,得到键值对应该存放的数组下标,然后调用 addEntry()函数把这个键值对插入到这个下标所在的链表中
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        if (size++ >= threshold)        // 如果键值对个数超过了HashMap当前容量的阈值
            resize(2 * table.length);    // 调用resize()函数进行扩容
    }

    在这个 addEntry() 函数中,会判断键值对个数是否超过了HashMap当前容量的阈值,如果超过了,则说明需要扩容,接下来就调用 resize() 函数扩容为原来的两倍。

    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);        // 把老数组中的所有键值对都拷贝到新数组中
        table = newTable;        // 修改老数组的指向,把老数组指向新数组,完成扩容
        threshold = (int)(newCapacity * loadFactor);
    }
    resize()函数会先创建一个新数组,然后调用 transfer() 函数把老数组中的所有键值对都拷贝到新数组中,最后修改老数组的指向,把老数组指向新数组,完成扩容。
    扩容过程中会出现循环链表的情况就是多个线程在执行 transfer() 函数导致的,下面看看 transfer() 函数的代码
    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];    // 记录下老数组第 j 个链表,接下来会链表上的键值对都拷贝到新数组
            if (e != null) {        // 如果链表不为空才需要拷贝
                src[j] = null;        // 先老数组第j个链表置为空链表
                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);
            }
        }
    }
    其中最关键的就是其中的 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

    现在需要对它进行扩容,扩容后数组大小为原来的两倍,创建一个大小为4的数组
    假设a、b两个数扩容后刚好又hash冲突了,即又在同一个链表中,所在下标为3;c在下标为1的链表中。下面开始扩容。
    e指针指向了老数组的第1个链表

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

    第二轮循环:

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

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

    多线程环境中扩容

    假设在第 二 次循环中的第二步(执行完e.next = newTable[i];)后当前线程的时间片刚好用完了,当前线程被挂起,这时刚好又有一个线程 P2 也来执行扩容操作,它并不会从第二步开始执行,而是重新从第一步开始执行,加入新线程后的扩容图为
    可以看到,线程2扩容之后的newTable中的单链表形成了一个环,后续执行get操作的时候,会触发死循环,引起CPU的100%问题。

    四.总结

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

    再次回到面试回忆中

    后面还问到除了成环,线程不安全还会导致什么情况发生,也不会,再次gg, 此时,不禁想问:您看我还有机会吗?
    没机会了,今天官网状态已经变灰了,彻底凉凉了。这真是个让人感到悲伤的故事。

    参考:

    Cyc2018
  • 相关阅读:
    Good Bye 2014 B. New Year Permutation(floyd )
    hdu 5147 Sequence II (树状数组 求逆序数)
    POJ 1696 Space Ant (极角排序)
    POJ 2398 Toy Storage (叉积判断点和线段的关系)
    hdu 2897 邂逅明下 (简单巴什博弈)
    poj 1410 Intersection (判断线段与矩形相交 判线段相交)
    HDU 3400 Line belt (三分嵌套)
    Codeforces Round #279 (Div. 2) C. Hacking Cypher (大数取余)
    Codeforces Round #179 (Div. 2) B. Yaroslav and Two Strings (容斥原理)
    hdu 1576 A/B (求逆元)
  • 原文地址:https://www.cnblogs.com/hi3254014978/p/14122731.html
Copyright © 2020-2023  润新知