美文网首页
hashmap在高并发下的死循环和数据丢失

hashmap在高并发下的死循环和数据丢失

作者: 凉风拂面秋挽月 | 来源:发表于2020-03-03 15:46 被阅读0次

    本文数据丢失和死循环均来自于jdk7版本。jdk8修复了死循环。

    数据丢失

    数据都是很好理解,在并发条件下,t1,t2同时向hashmap写入一个entry,且经过hash算法得到的key相同(hash冲突),此时若t1先写入,t2后写入,结果将是t2的entry覆盖掉t1的entry。

    死循环

    死循环问题比较复杂,主要产生死循环的场景出现在并发下同时触发数组扩容。也就是A,B同时进行Resize。当当前hashmap中的size超过数组长度*装载因子的时候,将会新建一个两倍于当前数组长度的数组,并把老数组中的元素重新放入新数组(当然需要每个entry的key重新hash确定在新数组的位置)。
    t1线程添加entry4触发resize,t2线程触发entry5触发resize


    触发扩容
    // 扩容核心方法,基本思想就是遍历数据,使用头插法将旧数组元素移到新数组。
     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);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i]; // 头插法,新节点next指向该位置首节点
                    newTable[i] = e; // 新元素归位
                    e = next; // 指向下一个节点,继续遍历
                }
            }
        }
    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);
        }
    

    假如此时线程B遍历到Entry3对象,刚执行完Entry<K,V> next = e.next;时间片用完,线程就被挂起。对于线程B来说:
    e = Entry3
    next = Entry2
    这时候线程A畅通无阻地进行着Rehash直接完成resize,在将entry3,entry2放入新的数组中,假设再次出现hash碰撞,由于采用头插法,最终结果应是entry2->entry3。当ReHash完成后,结果如下(图中的e和next,代表线程B的两个引用):


    A,B线程图

    这时候,两个线程都走到了ReHash的步骤。让我们回顾一下ReHash的代码:

     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);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i]; // 头插法,新节点next指向该位置首节点
                    newTable[i] = e; // 新元素归位
                    e = next; // 指向下一个节点,继续遍历
                }
            }
        }
    

    由于A线程的原因,entry2.next=entry3,entry3.next = null而在线程B中认为entry3.next=entry2(其实已经不是了,但此时e=entry3,next=entry2)。
    我们继续往下走,e=entry3,entry3放入新数组,然后next = entry2。


    image.jpg
    image.jpg

    接着entry2进入下一轮循环,此时e=entry2,next = entry3!(虽然看起来现在出了问题,但其实还没有)。
    我们继续执行到下图这一步。


    image.jpg

    entry2被放入线程b的数组的元素头结点,entry3被挤下去了。


    image.jpg
    如果是单线程,现在就结束了,因为entry2.next=null。但由于entry2.next=entry3(线程A的锅)。导致while循环并没有结束!
    entry3的循环又一次抢占头结点,把entry2挤下去,然后entry再次抢占头结点,把entry3挤下去。(因为此时形成了闭环)最终结果就是无限循环,cpu负载100%,程序崩溃。
    20180211102047225.jpg

    java8的改进

    1.添加了红黑树,当链表长度大于8时,会将链表转为红黑树。
    2.jdk7中的死循环原因来自于头插法,扩容复制后,如果在老数组中的链表hash冲突且复制到新数组后依然冲突,则会让老数组中的链表顺序倒置,导致多线程下有几率形成链表闭环。在jdk8中,扩容后,新数组中的链表顺序依然与旧数组中的链表顺序保持一致。具体JDK8是用 head 和 tail 来保证链表的顺序和之前一样(双指针保证插入顺序和链表顺序一致),这样就不会产生循环引用。也就没有死循环了。
    3.虽然修复了死循环的BUG,但是HashMap 还是非线程安全类,仍然会产生数据丢失等问题。

    相关文章

      网友评论

          本文标题:hashmap在高并发下的死循环和数据丢失

          本文链接:https://www.haomeiwen.com/subject/zrgikhtx.html