HashMap在java并发中如何发生死循环

        在多线程环境中,使用HashMap进行put操作时会引起死循环,导致CPU使用接近100%,下面通过代码分析一下为什么会发生死循环。

      首先先分析一下HashMap的数据结构:HashMap底层数据结构是有一个链表数据构成的,HashMap中定义了一个静态内部类作为链表,代码如下(与本文无关的代码省略):

 

 

静态内部类entry代码 

 收藏代码

  1.     static class Entry<K,V> implements Map.Entry<K,V> {  
  2.         final K key;  
  3.         V value;  
  4.         Entry<K,V> next;  
  5.         final int hash;  
  6.   
  7.         /**  
  8.          * Creates new entry.  
  9.          */  
  10.         Entry(int h, K k, V v, Entry<K,V> n) {  
  11.             value = v;  
  12.             next = n;  
  13.             key = k;  
  14.             hash = h;  
  15.         }  
  16. 、  
  17.     }  

 

 

   

Hashmap属性代码 

 收藏代码

  1. /**  
  2.  * The table, resized as necessary. Length MUST Always be a power of two.  
  3.  */  
  4. transient Entry[] table;  

    之所以会导致HashMap出现死循环是因为多线程会导致HashMap的Entry节点形成环链,这样当遍历集合时Entry的next节点用于不为空,从而形成死循环

  

 

    单添加元素时会通过key的hash值确认链表数组下标

   

    

Java代码 

 收藏代码

  1. public V put(K key, V value) {  
  2.     if (key == null)  
  3.         return putForNullKey(value);  
  4.       
  5.     //确认链表数组位置  
  6.     int hash = hash(key.hashCode());  
  7.     int i = indexFor(hash, table.length);  
  8.       
  9.     //如果key相同则覆盖value部分  
  10.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  11.         Object k;  
  12.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  13.             V oldValue = e.value;  
  14.             e.value = value;  
  15.             e.recordAccess(this);  
  16.             return oldValue;  
  17.         }  
  18.     }  
  19.   
  20.     modCount++;  
  21.     //添加链表节点  
  22.     addEntry(hash, key, value, i);  
  23.     return null;  
  24. }  

 

 

 

   下面看一下HashMap添加节点的实现

 

  

Java代码 

 收藏代码

  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.  //bucketIndex 通过key的hash值与链表数组的长度计算得出  
  3.     Entry<K,V> e = table[bucketIndex];  
  4.     //创建链表节点        
  5.   table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  6.     
  7.   //判断是否需要扩容  
  8.   if (size++ >= threshold)  
  9.             resize(2 * table.length);  
  10. }  

 

 

    以上部分的实现不会导致链路出现环链,环链一般会出现HashMap扩容是,下面看看扩容的实现:

  

  

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.           
  11.         transfer(newTable);//可能导致环链  
  12.           
  13.         table = newTable;  
  14.         threshold = (int)(newCapacity * loadFactor);  
  15. }  

 

 

  下面transfer的实现

 

 

Java代码 

 收藏代码

  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. }  

   这个方法的目的是将原链表数据的数组拷到新的链表数组中,拷贝过程中如果形成环链的呢?下面用一个简单的例子来说明一下:

 

 

  

Java代码 

 收藏代码

  1. public class InfiniteLoop {  
  2.   
  3.     static final Map<Integer, Integer> map = new HashMap<Integer, Integer>(2, 0.75f);  
  4.   
  5.     public static void main(String[] args) throws InterruptedException {  
  6.   
  7.         map.put(5, 55);  
  8.   
  9.         new Thread("Thread1") {  
  10.             public void run() {  
  11.                 map.put(7, 77);  
  12.                 System.out.println(map);  
  13.             };  
  14.         }.start();  
  15.   
  16.         new Thread("Thread2") {  
  17.             public void run() {  
  18.                 map.put(3, 33);  
  19.                 System.out.println(map);  
  20.             };  
  21.         }.start();  
  22.   
  23.     }  
  24.   
  25. }  

 

 

   下面通过debug跟踪调试来看看如果导致HashMap形成环链,断点位置:

  1. 线程1的put操作
  2. 线程2的put操作
  3. 线程2的输出操作
  4. HashMap源码transfer方法中的第一行、第六行、第九行

    测试开始

  

  1.  使线程1进入transfer方法第一行,此时map的结构如下

    

 

    2.  使线程2进入transfer方法第一行,此时map的结构如下:

   



 
 3.接着切换回线程1,执行到transfer的第六行,此时map的结构如下:

 




 
 

4.然后切换回线程2使其执行到transfer方法的第六行,此时map的结够如上

5.接着切换回线程1使其执行到transfer方法的第九行,然后切换回线程2使其执行完,此时map的结构如下:

 



 6.切换回线程1执行循环,因为线程1之前是停在HashMap的transfer方法的第九行处,所以此时transfer方法的节点e的key=3,e.next的key=7

 

Java代码 

 收藏代码

  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);//线程1等线程2执行结束后  
  11.                                                           //从此处开始执行  
  12.                                                           //此时e的key=3,e.next.key=7  
  13.                                                           //但是此时的e.next.next的key=3了  
  14.                                                           //(被线程2修改了)  
  15.                     e.next = newTable[i];  
  16.                     newTable[i] = e;  
  17.                     e = next;  
  18.                 } while (e != null);  
  19.             }  
  20.         }  
  21. }  

  

  下面线程1开始执行第一次循环,循环后的map结构如下:

 

   

 

接着执行第二次循环:e.key=7,e.next.key=3,e.next.next=null

 



 

接着执行第三次循环,从而导致环链形成,map结构如下

 



 并且此时的map中还丢失了key=5的节点

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值