`

hashmap死循环

    博客分类:
  • java
 
阅读更多

本文受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. }  
    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;
    }

 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. }  
    void addEntry(int hash, K key, V value, int bucketIndex) {
	Entry<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操作:

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. }  
    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操作,闭合的回路就是在这里产生的:

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.     }  
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的位置,

Java代码 复制代码 收藏代码
  1. e.next = newTable[i];   
  2. newTable[i] = e;  
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这种死循环的问题。

分享到:
评论

相关推荐

    深入了解JAVA HASHMAP的死循环

    解决HashMap死循环的方法主要有以下几点: 1. 使用ConcurrentHashMap替代HashMap,保证线程安全。 2. 如果不能更换数据结构,可以使用synchronized关键字对HashMap进行同步控制,但这将降低并发性能。 3. 通过使用...

    疫苗:Java HashMap的死循环

    Java HashMap的死循环原因分析 HashMap是Java中一种常用的数据结构,它提供了快速的查找、插入和删除操作。然而,在多线程环境中使用HashMap可能会导致死循环的问题。下面我们来分析HashMap的死循环原因。 首先,...

    HashMap扩容死循环问题源码分析.mp4

    详 解 hashmap 1.7 扩 容 机 制 的 数 据 迁 移 以 及 出 现 环 形 列 表 导 致 死 锁 情 况 视 频

    java7hashmap源码-backend-study:后端学习之路

    java7 hashmap源码 随着Java学习的不断深入,发现...多线程下,hashmap的resize()方法为什么容易出现死循环? 答: 其他面试题? 答: 并发 概述 :star::star: :star::star: 线程池 :star: AQS :star: 锁 ListenalbeFut

    java面试精选必备题集

    + 死循环:多线程同时put操作,导致HashMap死循环 * HashMap出现Hash DOS攻击的问题 + Hash DOS攻击:大量相同哈希码的攻击 * ConcurrentHashMap的工作原理及代码实现 + 基于哈希表,线程安全 + 分段锁,提高...

    基于Java HashMap的死循环的启示详解

    Java HashMap的死循环是一个在多线程环境下容易出现的问题,主要由于其内部的迭代器实现方式和并发操作不当引起。本文将深入分析这个问题,并探讨如何避免这类错误。 首先,Java HashMap在非线程安全的环境下,如果...

    马士兵老师HashMap学习笔记

    本文将结合马士兵老师的讲解,详细阐述HashMap在不同JDK版本中的put操作流程,以及可能遇到的死循环问题。 首先,我们来看JDK 8中HashMap的put操作。在JDK 8中,HashMap进行了重大的优化,引入了红黑树(Red-Black ...

    关于如何解决HashMap线程安全问题的介绍

    在多线程环境下,两个线程同时触发扩容可能导致循环链表的形成,从而引发死循环,这是一种严重的性能问题。 为了解决HashMap的线程不安全问题,我们可以采取以下几种策略: 1. 使用Collections.synchronizedMap()...

    2021 java面试题.pdf

    HashMap死循环可能由迭代器的并发修改引起,ConcurrentHashMap是线程安全的HashMap实现。 静态代理和动态代理的区别在于静态代理需要手动编写代理类,而动态代理可以在运行时动态生成代理类。 JDK动态代理基于接口...

    Java后端技术面试基础汇总

    - **多线程情况下HashMap死循环的问题:** - 在并发环境下,如果扩容时多个线程同时修改,可能导致环形链表,从而死循环。 - **ConcurrentHashMap的工作原理:** - 使用分段锁技术,将数据分成多个段,每个段都有...

    Java后端技术面试汇总-2019

    - **多线程情况下HashMap死循环的问题**:当多个线程同时进行put操作时可能导致循环链表形成闭环。 - **HashMap出现HashDOS攻击的问题**:恶意构造大量相同的哈希值导致性能下降。 - **ConcurrentHashMap的工作...

    高级程序员必会的HashMap的线程安全问题,适用于0~2年的.7z

    3. **死循环(死锁)**:在极端情况下,由于HashMap的迭代器依赖于table的状态,如果在迭代过程中table结构发生变化(比如resize),可能会造成迭代器陷入死循环。 为了解决这些问题,有以下几种策略: 1. **使用...

    HashMap put方法的源码分析

    从Java 1.7到1.8,HashMap经历了重大改进,尤其是在解决死循环问题上。本文将深入解析Java 1.8中HashMap的put方法源码,探讨其内部工作原理。 首先,我们了解HashMap的基本结构。在Java 1.8中,HashMap由数组加链表...

    HashMap资料.zip

    线程安全问题可能导致数据不一致或者死循环等问题。 5. **null键和值**:HashMap允许键和值为null,但只能有一个键为null,因为所有null键的哈希值都为0,所有值为null的键都会被存放在数组的第0个槽位。 6. **...

    jdk1.7 HashMap中的致命错误:循环链表

    然而,在JDK1.7版本中,HashMap存在一个严重的问题,即“循环链表”(Looping List),这可能导致在多线程环境下性能急剧下降,甚至引发死循环。本文将深入探讨这个问题及其解决方案。 首先,我们来看看JDK1.7 ...

    并发下的 HashMap 为什么会引起死循环???.zip

    计算机技术、IT咨询、人工智能AI理论介绍,学习参考资料计算机技术、IT咨询、人工智能AI理论介绍,学习参考资料计算机技术、IT咨询、人工智能AI理论介绍,学习参考资料计算机技术、IT咨询、人工智能AI理论介绍,学习...

    Hashmap实现了Map接口的底层实现.docx

    HashMap在多线程环境中并不安全,因为其非线程安全的设计可能导致死循环。例如,在并发扩容时,可能产生环形链表。在这种情况下,尝试获取不存在的键时,可能会导致无限循环。因此,多线程场景推荐使用...

    hashmap-thread-test:测试 Java HashMap 是否是线程安全的

    - **死循环**:在扩容过程中,如果多个线程同时参与,可能导致链表形成循环,从而引发死循环。 - **并发修改异常**:使用`ConcurrentModificationException`,Java会尝试阻止这种情况发生,但并不总是有效。 ### ...

Global site tag (gtag.js) - Google Analytics