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操作:

Java代码   收藏代码
  1. public V put(K key, V value) {  
  2.     if (key == null)  
  3.         return putForNullKey(value);  
  4.     int hash = hash(key.hashCode());  
  5.     int i = indexFor(hash, table.length);  
  6.     //存在key,则替换掉旧的value  
  7.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  8.         Object k;  
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  10.             V oldValue = e.value;  
  11.             e.value = value;  
  12.             e.recordAccess(this);  
  13.             return oldValue;  
  14.         }  
  15.     }  
  16.     modCount++;  
  17.     //table[i]为空,这时直接生成一个新的entry放在table[i]上  
  18.     addEntry(hash, key, value, i);  
  19.     return null;  
  20. }  

 addEntry操作:

Java代码   收藏代码
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2. ry<K,V> e = table[bucketIndex];  
  3.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.     if (size++ >= threshold)  
  5.         resize(2 * table.length);  
  6. }  

 可以看到,如果现在size已经超过了threshold,那么就要进行resize操作:

Java代码   收藏代码
  1. void resize(int newCapacity) {  
  2.     Entry[] oldTable = table;  
  3.     int oldCapacity = oldTable.length;  
  4.     if (oldCapacity == MAXIMUM_CAPACITY) {  
  5.         threshold = Integer.MAX_VALUE;  
  6.         return;  
  7.     }  
  8.   
  9.     Entry[] newTable = new Entry[newCapacity];  
  10.     //将旧的Entry数组的数据转移到新的Entry数组上  
  11.     transfer(newTable);  
  12.     table = newTable;  
  13.     threshold = (int)(newCapacity * loadFactor);  
  14. }  

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

Java代码   收藏代码
  1. void transfer(Entry[] newTable) {  
  2.         Entry[] src = table;  
  3.         int newCapacity = newTable.length;  
  4.         /* 
  5.          * 在转换的过程中,HashMap相当于是把原来链表上元素的的顺序颠倒了。 
  6.          * 比如说 原来某一个Entry[i]上链表的顺序是e1->e2->null,那么经过操作之后 
  7.          * 就变成了e2->e1->null 
  8.          */  
  9.         for (int j = 0; j < src.length; j++) {  
  10.             Entry<K,V> e = src[j];  
  11.             if (e != null) {  
  12.                 src[j] = null;  
  13.                 do {  
  14.                     //我认为此处是出现死循环的罪魁祸首  
  15.                     Entry<K,V> next = e.next;  
  16.                     int i = indexFor(e.hash, newCapacity);  
  17.                     e.next = newTable[i];  
  18.                     newTable[i] = e;  
  19.                     e = next;  
  20.                 } while (e != null);  
  21.             }  
  22.         }  
  23.     }  

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

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

          enext
P1       e1e2
P2       e1e2

 

      此时两个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的位置,

Java代码   收藏代码
  1. e.next = newTable[i];  
  2. newTable[i] = e;  

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

 enext
P1e2e1

      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这种死循环的问题。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值