`

Java HashMap的死循环

阅读更多

在淘宝内网里看到同事发了贴说了一个CPU被100%的线上故障,并且这个事发生了很多次,原因是在Java语言在并发情况下使用HashMap造成 Race Condition,从而导致死循环。这个事情我4、5年前也经历过,本来觉得没什么好写的,因为Java的HashMap是非线程安全的,所以在并发下 必然出现问题。但是,我发现近几年,很多人都经历过这个事(在网上查“HashMap Infinite Loop”可以看到很多人都在说这个事)所以,觉得这个是个普遍问题,需要写篇疫苗文章说一下这个事,并且给大家看看一个完美的“Race Condition”是怎么形成的。

问题的症状

从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题。后来,我们的程序性能有问题,所以需要变 成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这个方法 上了,重启程序后问题消失。但是过段时间又会来。而且,这个问题在测试环境里可能很难重现。

我们简单的看一下我们自己的代码,我们就知道HashMap被多个线程操作。而Java的文档说HashMap是非线程安全的,应该用ConcurrentHashMap。

但是在这里我们可以来研究一下原因。

 

Hash表数据结构

我需要简单地说一下HashMap这个经典的数据结构。

HashMap通常会用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算法通过key算出这个数组的 下标i,然后就把这个<key, value>插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链 表。

我们知道,如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷(可参看《Hash Collision DoS 问题》)。

所以,Hash表的尺寸和容量非常的重要。一般来说,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过, 需要增大Hash表的尺寸,但是这样一来,整个Hash表里的无素都需要被重算一遍。这叫rehash,这个成本相当的大。

相信大家对这个基础知识已经很熟悉了。

HashMap的rehash源代码

下面,我们来看一下Java的HashMap的源代码。

Put一个Key,Value对到Hash表中:

 

  1. public V put(K key, V value)  
  2. {  
  3.     ......  
  4.     //算Hash值  
  5.     int hash = hash(key.hashCode());  
  6.     int i = indexFor(hash, table.length);  
  7.     //如果该key已被插入,则替换掉旧的value (链接操作)  
  8.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  9.         Object k;  
  10.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  11.             V oldValue = e.value;  
  12.             e.value = value;  
  13.             e.recordAccess(this);  
  14.             return oldValue;  
  15.         }  
  16.     }  
  17.     modCount++;  
  18.     //该key不存在,需要增加一个结点  
  19.     addEntry(hash, key, value, i);  
  20.     return null;  
  21. }  


检查容量是否超标

 

  1. void addEntry(int hash, K key, V value, int bucketIndex)  
  2. {  
  3.     Entry<K,V> e = table[bucketIndex];  
  4.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  5.     //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize  
  6.     if (size++ >= threshold)  
  7.         resize(2 * table.length);  
  8. }  


新建一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的Hash表中。

 

  1. void resize(int newCapacity)  
  2. {  
  3.     Entry[] oldTable = table;  
  4.     int oldCapacity = oldTable.length;  
  5.     ......  
  6.     //创建一个新的Hash Table  
  7.     Entry[] newTable = new Entry[newCapacity];  
  8.     //将Old Hash Table上的数据迁移到New Hash Table上  
  9.     transfer(newTable);  
  10.     table = newTable;  
  11.     threshold = (int)(newCapacity * loadFactor);  
  12. }  


迁移的源代码,注意高亮处:

 

  1. void transfer(Entry[] newTable)  
  2. {  
  3.     Entry[] src = table;  
  4.     int newCapacity = newTable.length;  
  5.     //下面这段代码的意思是:  
  6.     //  从OldTable里摘一个元素出来,然后放到NewTable中  
  7.     for (int j = 0; j < src.length; j++) {  
  8.         Entry<K,V> e = src[j];  
  9.         if (e != null) {  
  10.             src[j] = null;  
  11.             do {  
  12.                 Entry<K,V> next = e.next;  
  13.                 int i = indexFor(e.hash, newCapacity);  
  14.                 e.next = newTable[i];  
  15.                 newTable[i] = e;  
  16.                 e = next;  
  17.             } while (e != null);  
  18.         }  
  19.     }  
  20. }  

好了,这个代码算是比较正常的。而且没有什么问题。

正常的ReHash的过程

画了个图做了个演示。

  • 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
  • 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
  • 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程

并发下的Rehash

1)假设我们有两个线程。我用红色和浅蓝色标注了一下。

我们再回头看一下我们的 transfer代码中的这个细节:

 

  1. do {  
  2.     Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了  
  3.     int i = indexFor(e.hash, newCapacity);  
  4.     e.next = newTable[i];  
  5.     newTable[i] = e;  
  6.     e = next;  
  7. while (e != null);  

而我们的线程二执行完成了。于是我们有下面的这个样子。

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

2)线程一被调度回来执行。

  • 先是执行 newTalbe[i] = e;
  • 然后是e = next,导致了e指向了key(7),
  • 而下一次循环的next = e.next导致了next指向了key(3)

3)一切安好。

线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移

4)环形链接出现。

e.next = newTable[i] 导致  key(3).next 指向了 key(7)

注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。

分享到:
评论

相关推荐

    疫苗:Java HashMap的死循环

    疫苗:Java HashMap的死循环

    深入了解JAVA HASHMAP的死循环

    HASHMAP基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)下面小编来带大家详细了解下吧

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

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

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

    本篇文章是对Java HashMap的死循环进行了详细的分析介绍,需要的朋友参考下

    HashMap put方法的源码分析

    java1.7 HashMap用的是数组+链表实现的,同时采用的头插入法,存在死循环的问题 java1.8 HashMap用的是数组+链表+红黑树实现的,采用尾插法实现的,解决了死循环的问题,今天分析的就是1.8 // 初始容量为16 ...

    Java集合框架完整说明便于了解集合

    HashMap 和 Hashtable 的区别,HashSet如何检查重复,HashMap的底层实现,HashMap 多线程操作导致死循环问题,ConcurrentHashMap 和 Hashtable 的区别,ConcurrentHashMap线程安全的具体实现⽅式/底层具体实现,...

    大厂真题之蚂蚁金服-Java高级.zip

    但是 hashmap1.7 跟 1.8 中都没有任何同步操作,容易出现并发问题,甚至出现死循环 导致系统不可用。解决方案是 jdk 的 ConcurrentHashMap,位于 java.util.concurrent 下,专门 解决并发问题。

    疯狂JAVA讲义

    第1章 Java概述 1 1.1 Java语言的发展简史 2 1.2 Java的竞争对手及各自优势 4 1.2.1 C#简介和优势 4 1.2.2 Ruby简介和优势 4 1.2.3 Python的简介和优势 5 1.3 Java程序运行机制 5 1.3.1 高级语言的运行机制 6...

    java 面试题 总结

    Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。 最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而...

    阿里巴巴,天猫,支付宝面试题

    4. 用hashmap实现redis有什么问题(死锁,死循环,可用ConcurrentHashmap) 5. 线程的状态 5. 线程的阻塞的方式 6. sleep和wait的区别 7. hashmap的底层实现 8. 一万个人抢100个红包,如何实现(不用队列),如何...

    21天学通Java-由浅入深

    while循环语句举例 74 4.5 for循环语句 75 4.5.1 for循环语句的语法 75 4.5.2 用for循环来实现其他循环语句 76 4.5.3 for循环语句的举例 77 4.6 如何中断和继续语句的执行 78 4.6.1 break:中断语句执行 78 4.6.2 ...

    java面试题,180多页,绝对良心制作,欢迎点评,涵盖各种知识点,排版优美,阅读舒心

    多线程put后可能导致get死循环 67 多线程put的时候可能导致元素丢失 68 解决方案 68 【集合】ConcurrentHashMap的get(),put(),又是如何实现的?ConcurrentHashMap有哪些问题?ConcurrentHashMap的锁是读锁还是写锁...

    超级有影响力霸气的Java面试题大全文档

    Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。 最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而...

    sesvc.exe 阿萨德

    但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。 final HashMap, String&gt; map = new HashMap, String&gt;(); for (int i = 0; i ; i++) { new Thread(new Runnable() { @Override public...

    工程硕士学位论文 基于Android+HTML5的移动Web项目高效开发探究

    工程硕士学位论文 ...研究生姓名: 唐帅 导师姓名: 罗军舟 教授 苏生 教授 申请学位类别 工 程 硕 士 学位授予单位 东 南 大 学 工程领域名称 软 件 工 程 论文答辩日期 ...学位授予日期 答辩委员会主席 评阅人 ...

Global site tag (gtag.js) - Google Analytics