hashmap死循环原因总结

简介: hashmap死循环原因总结

本文受http://pt.alibaba-inc.com/wp/dev_related_969/hashmap-result-in-improper-use-cpu-100-of-the-problem-investigated.html 的启发,引用了其中的思想,对此表示感谢。

        来到杭州实习有一段日子了,很长时间都没有更新博客了,前几天,闲来无事,随便翻了一本书,毕玄的《分布式JAVA应用》,在看到HashMap那一节的时候,其中提到了HashMap是非线程安全的,在并发场景中如果不保持足够的同步,就有可能在执行HashMap.get时进入死循环,将CPU的消耗到100%。HashMap是线程不安全的,这个我知道的,但是在get操作会出现死循环,我还是第一次听说到。于是我google了一下,网上讨论的很多,原来很多人对这个都感兴趣啊,于是我深入到HashMap的源码去探究了一下。

      大家都知道,HashMap采用链表解决Hash冲突,具体的HashMap的分析可以参考一下http://zhangshixi.iteye.com/blog/672697 的分析。因为是链表结构,那么就很容易形成闭合的链路,这样在循环的时候就会产生死循环。但是,我好奇的是,这种闭合的链路是如何形成的呢。在单线程情况下,只有一个线程对HashMap的数据结构进行操作,是不可能产生闭合的回路的。那就只有在多线程并发的情况下才会出现这种情况,那就是在put操作的时候,如果size>initialCapacity*loadFactor,那么这时候HashMap就会进行rehash操作,随之HashMap的结构就会发生翻天覆地的变化。很有可能就是在两个线程在这个时候同时触发了rehash操作,产生了闭合的回路。下面我们从源码中一步一步地分析这种回路是如何产生的。先看一下put操作:

public V put(K key, V value) {   
    if (key == null)   
        return putForNullKey(value);   
    int hash = hash(key.hashCode());   
    int i = indexFor(hash, table.length);   
    //存在key,则替换掉旧的value   
    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++;   
    //table[i]为空,这时直接生成一个新的entry放在table[i]上   
    addEntry(hash, key, value, i);   
    return null;   
}  
void addEntry(int hash, K key, V value, int bucketIndex) {  
ry<K,V> e = table[bucketIndex];  
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    if (size++ >= threshold)  
        resize(2 * table.length);  
}  

可以看到,如果现在size已经超过了threshold,那么就要进行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];   
    //将旧的Entry数组的数据转移到新的Entry数组上   
    transfer(newTable);   
    table = newTable;   
    threshold = (int)(newCapacity * loadFactor);   
}  

看一下transfer操作,闭合的回路就是在这里产生的:

void transfer(Entry[] newTable) {   
        Entry[] src = table;   
        int newCapacity = newTable.length;   
        /*  
         * 在转换的过程中,HashMap相当于是把原来链表上元素的的顺序颠倒了。  
         * 比如说 原来某一个Entry[i]上链表的顺序是e1->e2->null,那么经过操作之后  
         * 就变成了e2->e1->null  
         */  
        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);   
            }   
        }   
    }  

那么回路究竟是如何产生的呢,问题就出在next=e.next这个地方,在多线程并发的环境下,为了便于分析,我们假设就两个线程P1,P2。src[i]的链表顺序是e1->e2->null。我们分别线程P1,P2的执行情况。

       首先,P1,和P2进入到了for循环中,这时候在线程p1和p2中,局部变量分别如下:

           e next
P1        e1 e2
P2        e1 e2

 

     此时两个Entry的顺序是依然是最开始的状态e1->e2->null,  但是此时p1可能某些原因线程暂停了,p2则继续执行,并执行完了do while循环。这时候Entry的顺序就变成了e2->e1->null。在等到P2执行完之后,可能p1才继续执行,这时候在P1线程中局部变量e的值为e1,next的值为e2(注意此时两个元素在内存中的顺序变成了e2->e1->null),下面P1线程进入了do while循环。这时候P1线程在新的Entry数组中找到e1的位置,

e.next = newTable[i];   
newTable[i] = e;  

下面会把next赋值给e,这时候e的值成为了e2,继续下一次循环,这时候

  e next
P1 e2 e1

     e2->next=e1,这个是线程P2的"功劳"。程序执行完这次循环之后,e=e1,

继续第三次循环,这时候根据算法,就会进行e1->next=e2。

     这样在线程P1中执行了e1->next=e2,在线程P2中执行了e2->next=e1,这样就形成了一个环。在get操作的时候,next值永远不为null,造成了死循环。

        实际上,刚开始我碰到这个说法的时候,还被吓了一跳,HashMap怎么还会出现这个问题呢,仔细分析一下,这个问题再高并发的场景下是很容易出现的。Sun的工程师建议在这样的场景下应采用ConcurrentHashMap。具体参考http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457

       虽然这个问题再平时的工作中还没有遇到,但是以后需要注意。要在不同的场景下选择合适的类,规避类似HashMap这种死循环的问题。

目录
相关文章
|
2月前
|
设计模式 安全 Java
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
假如有T1、T2两个线程同时对某链表扩容,他们都标记头结点和第二个结点,此时T2阻塞,T1执行完扩容后链表结点顺序反过来,此时T2恢复运行再进行翻转就会产生环形链表,即B.next=A;采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。JDK8中,HashMap采用尾插法,扩容时链表节点位置不会翻转,解决了扩容死循环问题,但是性能差了一点,因为要遍历链表再查到尾部。例如15(即2^4-1)的二进制为1111,31的二进制为11111,63的二进制为111111,127的二进制为1111111。
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
|
1月前
|
Java 调度
HashMap为什么会死循环?
本文分析了在Java中HashMap导致死循环的原因,主要由于在JDK 1.7及之前版本中,多线程环境下进行扩容操作时,头插法导致的链表反转,以及线程调度问题,从而形成循环链表。
23 0
HashMap为什么会死循环?
|
1月前
|
存储 算法 安全
HashMap常见面试题(超全面):实现原理、扩容机制、链表何时升级为红黑树、死循环
HashMap常见面试题:红黑树、散列表,HashMap实现原理、扩容机制,HashMap的jd1.7与jdk1.8有什么区别,寻址算法、链表何时升级为红黑树、死循环
|
存储 安全 容器
为什么HashMap会产生死循环?
HashMap死循环是一个比较常见、也是比较经典的面试题,在大厂的面试中也经常被问到。HashMap的死循环问题只在JDK1.7版本中会出现,主要是HashMap自身的工作机制,再加上并发操作,从而导致出现死循环。JDK1.8以后,官方彻底解决了这个问题。
104 0
|
算法 安全 Java
【Java面试】HashMap死循环问题
【Java面试】HashMap死循环问题
74 0
|
存储 安全 Java
聊聊hashmap在1.7情况下的多线程死循环问题
在Java 1.7版本中,HashMap的扩容过程存在一个多线程环境下的死循环问题。这个问题的具体原因是由于HashMap在进行扩容时,多个线程同时进行put操作,可能会导致链表形成环形结构,从而导致get操作陷入死循环。
173 0
|
算法 安全 Java
推荐:并发情况下:Java HashMap 形成死循环的原因
推荐:并发情况下:Java HashMap 形成死循环的原因
178 1
|
监控 Java 网络安全
为什么JDK8中HashMap依然会死循环
为什么JDK8中HashMap依然会死循环
为什么JDK8中HashMap依然会死循环
|
Java
天猫面试官:说说高并发下的HashMap的死循环是怎么形成的!
师傅,我常常听别人说,不要在并发情况下使用HashMap,可能会出现死循环,这个死循环是怎么形成的呢?
125 0
|
算法 安全 Java
Java并发编程 - HashMap 死循环
Java并发编程 - HashMap 死循环
174 0
Java并发编程 - HashMap 死循环