hashmap引发死循环

今天开发环境压测的时候出现cpu用满了状况,看线程堆栈,一堆线程都停留在org.apache.commons.collections4.map.AbstractHashedMap.put(AbstractHashedMap.java:285),查看google源代码html

public Object put(Object key, Object value) {
        key = convertKey(key);
        int hashCode = hash(key);
        int index = hashIndex(hashCode, data.length);
        HashEntry entry = data[index];
        while (entry != null) {
            if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) {
                Object oldValue = entry.getValue();
                updateEntry(entry, value);
                return oldValue;
            }
            entry = entry.next;
        }
        
        addMapping(index, hashCode, key, value);
        return null;
    }java

方法是非线程安全的,而addMapping方法会触发ensureCapacity扩容,而并发扩容就会容易致使死循环, 具体缘由参考文章https://www.cnblogs.com/dongguacai/p/5599100.html算法

解决方法:apache

一、使用ConcurrentHashMap。数组

二、使用Collections.synchronizedMap(Mao<K,V> m)方法把HashMap变成一个线程安全的Map。安全

 

 

我copy了部分文章内容并发

正常的扩容过程

咱们先来看下单线程状况下,正常的rehash过程app

一、假设咱们的hash算法是简单的key mod一下表的大小(即数组的长度)。测试

二、最上面是old hash表,其中HASH表的size=2,因此key=3,5,7在mod 2 之后都冲突在table[1]这个位置上了。google

三、接下来HASH表扩容,resize=4,而后全部的<key,value>从新进行散列分布,过程以下:

 

 

 

在单线程状况下,一切看起来都很美妙,扩容过程也至关顺利。接下来看下并发状况下的扩容。

并发状况下的扩容

一、首先假设咱们有两个线程,分别用红色和蓝色标注了。

二、扩容部分的源代码:

复制代码
 1 void transfer(Entry[] newTable) {  2 Entry[] src = table;  3 int newCapacity = newTable.length;  4 for (int j = 0; j < src.length; j++) {  5 Entry<K,V> e = src[j];  6 if (e != null) {  7 src[j] = null;  8 do {  9 Entry<K,V> next = e.next; 10 int i = indexFor(e.hash, newCapacity); 11 e.next = newTable[i]; 12 newTable[i] = e; 13 e = next; 14 } while (e != null); 15  } 16  } 17 }
复制代码

三、若是在线程一执行到第9行代码就被CPU调度挂起,去执行线程2,且线程2把上面代码都执行完毕。咱们来看看这个时候的状态:

 

 

四、接着CPU切换到线程一上来,执行8-14行代码,首先安置3这个Entry:

 

这里须要注意的是:线程二已经完成执行完成,如今table里面全部的Entry都是最新的,就是说7的next是3,3的next是null;如今第一次循环已经结束,3已经安置稳当。看看接下来会发生什么事情:

一、e=next=7;

二、e!=null,循环继续

三、next=e.next=3

四、e.next 7的next指向3

五、放置7这个Entry,如今如图所示:

放置7以后,接着运行代码:

一、e=next=3;

二、判断不为空,继续循环

三、next= e.next  这里也就是3的next 为null

四、e.next=7,就3的next指向7.

五、放置3这个Entry,此时的状态如图: 

这个时候其实就出现了死循环了,3移动节点头的位置,指向7这个Entry;在这以前7的next同时也指向了3这个Entry。

代码接着往下执行,e=next=null,此时条件判断会终止循环。此次扩容结束了。可是后续若是有查询(不管是查询的迭代仍是扩容),都会hang死在table【3】这个位置上。如今回过来看文章开头的那个Demo,就是挂死在扩容阶段的transfer这个方法上面。

出现上面这种状况毫不是我要在测试环境弄一批数据专门为了演示这种问题。咱们仔细思考一下就会得出这样一个结论:若是扩容前相邻的两个Entry在扩容后仍是分配到相同的table位置上,就会出现死循环的BUG。在复杂的生产环境中,这种状况尽管不常见,可是可能会碰到。

相关文章
相关标签/搜索