关于java:高并发下的HashMap为什么会死循环

前言

  HashMap并发状况下的死循环问题在jdk 1.7及之前版本存在的,jdk 1.8 通过减少loHead和loTail进行了修复,尽管进行了修复,然而如果波及到并发状况下,个别倡议应用CurrentHashMap代替HashMap来确保不会呈现线程平安问题。

  在jdk 1.7及之前 HashMap在并发状况下产生的循环问题,该循环问题将以致服务器的cpu飙升至100%,为了解答这个纳闷,那么明天就来理解一下线程不平安的HashMap在高并发的状况下是如何造成死循环的,要探索hashmap死循环的起因那就要从hashmap的源码开始进行剖析,这样能力从根本上对hashmap进行了解。 在剖析之前咱们要晓得在jdk 1.7版本及之前HashMap采纳的是数组 + 链表的数据结构,而在jdk 1.8则是采纳数组 + 链表 + 红黑树的数据结构以进一步升高hash抵触后带来的查问损耗。

注释

首先hashmap进行元素的插入这里会调用put办法

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);//调配数组空间
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//对key的hashcode进一步计算,确保散列平均
        int i = indexFor(hash, table.length);//获取在table中的理论地位
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {...}
        modCount++;//保障并发拜访时,若HashMap内部结构发生变化,疾速响应失败
        
        //重点关注这个addEntry减少元素的办法
        addEntry(hash, key, value, i);
        return null;
    }

紧接着咱们来看这个addEntry办法,外面调用的resize()扩容办法是明天的配角

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);//当size超过临界阈值threshold,并且行将产生哈希抵触时进行扩容,扩容后新容量为旧容量的2倍
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);//扩容后从新计算插入的地位下标
        }

        //把元素放入HashMap的桶的对应地位
        createEntry(hash, key, value, bucketIndex);
    }

上面咱们进入到resize()法中,再揭开外面的transfer()办法的面纱,这个办法也是造成死循环的罪魁祸首

//按新的容量扩容Hash表  
    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;//批改HashMap的底层数组  
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//批改阀值  
    }  

最初一起来仔细分析这个transfer()办法

//将老的表中的数据拷贝到新的构造中  
    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) {//如果是从新Hash,则须要从新计算hash值  
                    e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);//定位Hash桶  
                e.next = newTable[i];//元素连贯到桶中,这里相当于单链表的插入,总是插入在最后面
                newTable[i] = e;//newTable[i]的值总是最新插入的值
                e = next;//持续下一个元素  
            }  
        }  
    }

增加元素达到阀值后对hashmap进行扩容,走reaize办法,在对HashMap进行扩容时,又会调用一个transfer()对旧的hashmap中的元素进行转移,那么咱们明天要探索的死循环问题 就是产生在这个办法里的,在进行元素转移时transfer办法里会调用上面四行代码

Entry<K,V> next = e.next; 
e.next = newTable[i];
newTable[i] = e;
e = next;

把元素插入新的HashMap中,粗略的看下这四行代码仿佛并没有什么问题,元素进行转移的图如下(线程不抵触的状况下)

那么咱们让线程A、B同时拜访我这段代码,当现A线程执行到以下代码时

Entry<k,v> next = e.next;

线程A交出工夫片,线程B这时候接手转移并且实现了元素的转移,这个时候线程A又拿到工夫片并接着执行代码

执行后代码如图,当e = a时,这时候这时候再执行

e.next = newTable[i];// a元素指向了b元素,产生了循环

  在链表就就产生了循环后,当get()办法获取元素的时候正好落在这个循环的链表上时,线程会始终在环了遍历,无奈跳出,从而导致cpu飙升100%!

总结

在多线程状况下尽量不要用HashMap,能够用线程平安的hash表来代替如ConcurrentHashMap、HashTable、Collections.synchronizedMap()。来防止产生多线程平安问题。

/ 感激反对 /

以上便是本次分享的全部内容,心愿对你有所帮忙^_^

喜爱的话别忘了 分享、点赞、珍藏 三连哦~

欢送关注公众号 程序员巴士,一辆乏味、有范儿、有温度的程序员巴士,涉猎大厂面经、程序员生存、实战教程、技术前沿等内容,关注我,交个敌人吧!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理