`
vavi
  • 浏览: 15334 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

HashMap 多并发情况下死循环原因分析

阅读更多

  1. HashMap 主要属性
    1. transient Entry[] table;  Entry数组,Entry这个对象自身是个链表数据结构. 当hash发生冲撞时,则根据key放入的先后顺序放到链表中去.比如在java中,  Aa和BB这两个字符串的hash code 是一样的.这样当先后调用map.put("Aa")和map.put("BB")后,map的size会变成2,但是这两个元素会放在同一个Entry[i]中,
    2. transient int size ;  map集合的元素数目
    3. int threshold ; 当你不断地从map中put 元素后, map 的size 会不断增大;这样就会存在一个问题,假如这样无限制put下去,最终会超过map集合的最大容量导致程序出错.所以,在HashMap的实现中,当map的size 大到一定值后,会自动将map的容量扩大.而这里的"当map的size 大到一定值后","一定值"即为 threshold,map容量扩大的阈值,该值默认是 capacity(容量) * load factor(负载因子) 
    4. static final int DEFAULT_INITIAL_CAPACITY = 16;  //MUST be a power of two. capacity 该值必须是2的指数,主要是用于static int indexFor(int h, int length),具体分析到该方法时再详细阐述
  2. HashMap 主要方法
    1. get 方法
      public V get(Object key) { 
              if (key == null) 
                  return getForNullKey(); 
              int hash = hash(key.hashCode());//1.对key的hash code计算hash值 
              for (Entry<K,V> e = table[indexFor(hash, table.length)]; //2.调用indexFor 方法获取对应数组元素下标 
                   e != null; 
                   e = e.next) { //3.获取数组里面的指定元素后,然后对该链表进行遍历 
                  Object k; 
                  if (e.hash == hash && ((k = e.key) == key || key.equals(k))) //4.找到"相同"的key后,则返回 
                      return e.value; 
              } 
              return null; 
          }
       
      1. 存在2个疑问,请大家赐教.
      2. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  为什么要重复判断 e.hash == hash ,因为put方法已经保证了目标查找元素与Entry[i]中的hash都是相等的; 写完这个问题,发现原因应该是:如果仅留一个key.equals(k), 可以覆写 key的 equals 方法,返回意料之外的值
      3. 为什么需要判断(k = e.key) == key?同样,写完这个问题后,发现原因应该是:存在 Map m = new HashMap();String  x = "Aa";m.put(x, 1);
      4. 这样的代码,这样就可以避免调用对象的equals方法,直接返回指定value即可.

      5. hash方法
         static int hash(int h) {  
                // This function ensures that hashCodes that differ only by 
                // constant multiples at each bit position have a bounded 
                // number of collisions (approximately 8 at default load factor). 
                h ^= (h >>> 20) ^ (h >>> 12); 
                return h ^ (h >>> 7) ^ (h >>> 4); 
            } 
         

      6. 目前对算法还是没有形成比较深刻的理解,等更加深入的理解再写吧

      7.   static int indexFor(int h, int length) { 
                return h & (length-1); 
            }//通过位与,16-1的二进制为1111 1111 然后 去 h的后8个bit 作为 数组的下标,这样可以迅速找到该hash值对应的数组索引
         

      8. put方法 
      9.  
      10. public V put(K key, V value) { 
               if (key == null) 
                    return putForNullKey(value); 
                int hash = hash(key.hashCode()); 
                int i = indexFor(hash, table.length); 
                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++; 
                addEntry(hash, key, value, i);//当前面一直没有匹配的key后,则把该k/v键值对放到map中
                return null; 
            }
         


      11.  
        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); //此时如果发生hash冲撞时,链表中的顺序和放入元素的顺序相反 
                if (size++ >= threshold)  //当大于阈值时,即 容量*因子 
                    resize(2 * table.length); 
            }
         



      12.  
        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]; 
                transfer(newTable); //需要将当前map中的元素复制到新的数组中 
                table = newTable;  
                threshold = (int)(newCapacity * loadFactor); 
            }
         


      13.  
        
        void transfer(Entry[] newTable) { 
                Entry[] src = table; 
                int newCapacity = newTable.length; 
                for (int j = 0; j < src.length; j++) {//遍历每个数组 
                    Entry<K,V> e = src[j]; //找到第i个数组元素 
                    if (e != null) { 
                        src[j] = null; //及时将不需要的元素置为空,对GC友好. 
                        do { 
                            Entry<K,V> next = e.next; //假设该链表中的元素是 "Aa"->"BB" ,那么next值为"BB" 
                            int i = indexFor(e.hash, newCapacity); //获取新数组元素位置,因为newCapacity变了,所以该元素在newTable中的顺序一般也会发生改变 
                            e.next = newTable[i]; //将e.next置为null 
                            newTable[i] = e; //然后将 newTable[i] 为 "Aa","Aa".next的值是null 
                            e = next; //此时e为"BB" TIPS:在实际分析时,可以从源码复制HashMap和AbstractMap 进行debug,便于看到HashMap内部的数据变化 
                        } while (e != null); 
                    } 
                } 
            }
        
         

      14. HashMap 冲撞


      15. static int hash(int h) {  
                // This function ensures that hashCodes that differ only by 
                // constant multiples at each bit position have a bounded 
                // number of collisions (approximately 8 at default load factor). 
                h ^= (h >>> 20) ^ (h >>> 12); 
                return h ^ (h >>> 7) ^ (h >>> 4); 
            } 
        
        
         

        1)在Java里, Aa和BB这两个字符串的hash code(或hash key) 是一样的,也就是Collision 。
        2)于是,我们就可以通过这两个种子生成更多的拥有同一个hash key的字符串。如:”AaAa”, “AaBB”, “BBAa”, “BBBB”。这是第一次迭代。其实就是一个排列组合,写个程序就搞定了。
        3)然后,我们可以用这4个长度的字符串,构造8个长度的字符串,如下所示:
        "AaAaAaAa", "AaAaBBBB", "AaAaAaBB", "AaAaBBAa", "BBBBAaAa", "BBBBBBBB", "BBBBAaBB", "BBBBBBAa", "AaBBAaAa", "AaBBBBBB", "AaBBAaBB", "AaBBBBAa", "BBAaAaAa", "BBAaBBBB", "BBAaAaBB", "BBAaBBAa",
        4)同理,我们就可以生成16个长度的,以及256个长度的字符串,总之,很容易生成N多的这样的值。
        在攻击时,我只需要把这些数据做成一个HTTP POST 表单,然后写一个无限循环的程序,不停地提交这个表单。这样在大量请求下容易导致系统响应缓慢CPU100%
        之所以出现了CPU100%,是因为在并发情况下出现了死循环.下文产生死循环的原因假设map中的容量为2,并且存在一个"Aa"元素,然后2个线程 T1,T2,同时调用了 map.put("BB"),然后会接着transfer方法 ,同时进入了下面的for循环.这里有个小细节,map.put方法在调用resize方法后,会将链表中的元素逆向后放入.如 原来是 Aa->BB,resize后变成BB->Aa假设当前map元素是 BB->Aa ;T1因为某种原因在for循环口暂停,T2顺利执行完毕,此时在执行Entry<K,V> e = src[j];暂停(T1线程内的元素顺序是BB ->Aa ,e 为 BB,next 为 AA); T2顺利执行完毕,此时这个被多线程共享的map元素顺序依此是  Aa->BBT1恢复执行,在执行e.next = newTable[i];这句话时,BB的next为新table种的Aa元素,而此时新table的next元素是BB,这样 "环"就形成了. BB->Aa-BB  . (在分析的时候,要注意他们之间都是引用传递.这样要好理解些.) 
      16. void transfer(Entry[] newTable) { 
                Entry[] src = table; 
                int newCapacity = newTable.length; // newTable 在2个线程内互相独立 
                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); 
                    } 
                } 
            }
        
        
        
        
         
    1.  http://coolshell.cn/articles/6424.html Hash Collision DoS 问题  
    2. http://www.iteye.com/topic/962172  HashMap 死循环的探究
    3. HashMap深度分析
分享到:
评论
1 楼 vavi 2012-11-06  
要上班了,等回来有空再排下版.. 这个编辑器真难用..

相关推荐

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

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

    深入了解JAVA HASHMAP的死循环

    然而,由于HashMap不是线程安全的,因此在多线程环境下直接使用可能会引发一系列问题,其中之一就是所谓的"死循环"。本文将深入探讨这一主题。 首先,死循环问题通常发生在并发环境中,当多个线程同时修改HashMap时...

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

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

    马士兵老师HashMap学习笔记

    然而,由于HashMap是非线程安全的,如果多个线程同时put可能导致死循环的问题。这是因为当两个线程同时put并产生哈希冲突时,它们可能会在链表中形成环形结构,导致迭代器遍历时无法正常结束。 为了解决这个问题,...

    HashMap源码分析系列-第四弹:HashMap多线程解决方案.docx

    1. **链表死循环问题**:在JDK 1.7中,当多个线程同时进行`put`操作时,可能会出现链表死循环的情况,这是一个严重的BUG。这个问题在JDK 1.8中通过使用尾插法得到了解决。 2. **数据覆盖问题**:如前所述的例子所示...

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

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

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

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

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

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

    并发编程之一 日常学习笔记

    在并发编程中,不合适的并发操作HashMap可能导致数据不一致或者死循环。了解HashMap的内部实现有助于开发者理解为什么需要使用ConcurrentHashMap这样的线程安全数据结构,以及如何在多线程环境中正确使用HashMap。 ...

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

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

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

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

    面试题之详解HashMap

    本文将详细解析HashMap的一些常见面试题,包括HashMap的长度为什么是2的幂次方、多线程操作下的死循环问题、底层实现以及扩容机制。 1. HashMap长度为什么是2的幂次方? 这个设计主要是为了优化哈希函数的效率。在...

    第9讲 对比Hashtable、HashMap、TreeMap有什么不同?1

    在并发环境下,HashMap的非线程安全可能导致死循环(例如,多个线程同时进行扩容操作)和其他并发问题。 了解Map的整体结构也很重要,HashMap和其他Map实现如LinkedHashMap(保持插入顺序或访问顺序的HashMap)都是...

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

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

    java7-8中的 HashMap和ConcurrentHashMap全解析

    `HashMap`是非线程安全的,意味着在多线程环境下,多个线程同时操作`HashMap`可能会导致数据不一致或者死循环。因此,如果需要在并发环境中使用,必须使用同步机制,如`synchronized`关键字或`Collections....

    Java并发程序设计+并发

    Java并发程序设计是Java开发中的重要领域,它涉及到如何在多线程环境下高效、安全地执行代码。在Java中,并发编程主要通过类库、工具和技术来实现,这些包括线程、锁、同步机制以及并发容器等。下面将详细介绍Java...

    ConcurrentHashMap的实现原理(JDK1.7和JDK1.8).pdf

    在多线程环境中,如果多个线程同时修改HashMap,可能会引发死循环,导致CPU利用率极高。 - `HashTable`是线程安全的,但其线程安全的实现方式过于简单,所有操作都使用`synchronized`关键字,这使得在高并发环境下...

    集合常见面试题

    jdk1.8之前并发操作hashmap时为什么会有死循环的问题? hashmap扩容时每个entry需要再计算一次hash吗? hashmap的数组长度为什么要保证是2的幂? 如何用LinkedHashMap实现LRU? 如何用TreeMap实现一致性hash? ...

Global site tag (gtag.js) - Google Analytics