zoukankan      html  css  js  c++  java
  • Hashmap扩容时死循环问题

    hashmap扩容时死循环问题

    源码如下 ——–Put一个Key,Value对到Hash表中:

    public V put(K key, V value)
    {
        ......
        //计算Hash值
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        //各种校验吧
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //该key不存在,需要增加一个结点
        addEntry(hash, key, value, i);
        return null;
    }

    这里添加一个节点需要检查是否超出容量,出现了一个负载因子。

    void addEntry(int hash, K key, V value, int bucketIndex)
    {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
        if (size++ >= threshold)
            resize(2 * table.length);//扩容都是2倍2倍的来的,
    }
    1.奇数不行的解释很能被接受,在计算hash的时候,确定落在数组的位置的时候,计算方法是(n - 1) & hash ,奇数n-1为偶数,偶数2进制的结尾都是0,经过&运算末尾都是0,会 增加hash冲突。
    2.为啥要是2的幂,不能是2的倍数么,比如6,10? 2.1 hashmap 结构是数组,每个数组里面的结构是node(链表或红黑树),正常情况下,如果你想放数据到不同的位置,肯定会想到取余数确定放在那个数据里,
    计算公式: hash % n,这个是十进制计算。在计算机中, (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,计算更加高效。
    2.2 只有是2的幂数的数字经过n-1之后,二进制肯定是 ...11111111 这样的格式,这种格式计算的位置的时候,完全是由产生的hash值类决定,而不受n-1(组数长度) 影响。
    你可能会想, 受影响不是更好么,又计算了一下,类似于扰动函数,hash冲突可能更低了,这里要考虑到扩容了,2的幂次方*2,在二进制中比如4和8,代表2的2次方和3次方,他们的2进制结构相 似,
    比如 4和8 00000100 0000 1000 只是高位向前移了一位,这样扩容的时候,只需要判断高位hash,移动到之前位置的倍数就可以了,免去了重新计算位置的运算。


    既然新建了一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的Hash表中。

    void resize(int newCapacity)
    {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        ......
        //创建一个新的Hash Table
        Entry[] newTable = new Entry[newCapacity];
        //将Old Hash Table上的数据迁移到New Hash Table上
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

    好,重点在这里面的transfer()!

    void transfer(Entry[] newTable)
    {
        Entry[] src = table;
        int newCapacity = newTable.length;
        //下面这段代码的意思是:
        //  从OldTable里摘一个元素出来,然后放到NewTable中
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

    假设有一个hashMap数组(正常是2的N次长度,这里方便举例), 节点3上存有abc元素,此时发生扩容



    此时假设有两个线程

    线程B在执行到Entry<K,V> next = e.next;后挂起,此时e指向元素a,e.next指向元素b

    到线程A在new table的数组7位置依次用头插法插入3个元素后

     此时线程B继续执行以下代码

    Entry<K,V> next = e.next;   //next = b
     e.next = newTable[i];    //数组7的地址赋予变量e.next
    newTable[i] = e; //将a放到数组7的位置
    e = next;  //  e = next = b
    执行结束的关系如图

    变量e = b不是null,循环继续执行,

    Entry<K,V> next = e.next;  //  next = a
     e.next = newTable[i];  //数组7地址指向e.next
     newTable[i] = e; //将b放到数组7的位置
     e = next;    //e =next = a

    执行后引用关系图

     此时变量e = a仍旧不为空,继续循环。。

    Entry<K,V> next = e.next;  //  变量a没有next,所以next = null
     e.next = newTable[i];  // 因为newTable[i]存的是b,这一步相当于将a的next指向了b,于是问题出现了
    newTable[i] = e; //将变量a放到数组7的位置
    e
    = next; // e= next = null



    当在数组7遍历节点寻找对应的key时, 节点a和b就发生了死循环, 直到cpu被完全耗尽。

    另外,如果最终线程2执行了table = newTable;那元素C就发生了数据丢失问题

    四.总结

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


  • 相关阅读:
    Mybatis学习笔记(八) —— Mybatis整合spring
    Mybatis学习笔记(七) —— 关联查询
    Mybatis学习笔记(六) —— 动态sql
    Mybatis学习笔记(五) —— Mapper.xml(输入映射和输出映射)
    Mybatis学习笔记(四) —— SqlMapConfig.xml配置文件
    Mybatis学习笔记(三) —— DAO开发方法
    Mybatis学习笔记(二) —— mybatis入门程序
    Mybatis学习笔记(一) —— mybatis介绍
    tomcat的热部署配置
    int bool 字符串 列表 字典 集合
  • 原文地址:https://www.cnblogs.com/lanqingzhou/p/13600908.html
Copyright © 2011-2022 走看看