为什么80%的码农都做不了架构师?>>>   

在淘宝内网里看到同事发了贴说了一个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public     V put(K key, V value)
{
         ......
         //算Hash值
         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++;
         //该key不存在,需要增加一个结点
         addEntry(hash, key, value, i);
         return     null     ;
}

检查容量是否超标

1
2
3
4
5
6
7
8
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);
         //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
         if     (size++ >= threshold)
             resize(     2     * table.length);
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void     transfer(Entry[] newTable)
{
         Entry[] src = table;
         int     newCapacity = newTable.length;
         //下面这段代码的意思是:
         //  从OldTable里摘一个元素出来,然后放到NewTable中
         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     );
             }
         }
}

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

正常的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
2
3
4
5
6
7
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     );

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

注意,因为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。

其它

有人把这个问题报给了Sun,不过Sun不认为这个是一个问题。因为HashMap本来就不支持并发。要并发就用ConcurrentHashmap

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457

我在这里把这个事情记录下来,只是为了让大家了解并体会一下并发环境下的危险。

参考:http://mailinator.blogspot.com/2009/06/beautiful-race-condition.html

(全文完)

 writes:"This is a classic symptom of an incorrectly synchronized use of
HashMap. Clearly, the submitters need to use a thread-safe
HashMap. If they upgraded to Java 5, they could just use
ConcurrentHashMap. If they can't do this yet, they can use
either the pre-JSR166 version, or better, the unofficial backport
as mentioned by Martin. If they can't do any of these, they can
use Hashtable or synchhronizedMap wrappers, and live with poorer
performance. In any case, it's not a JDK or JVM bug."I agree that the presence of a corrupted data structure alone
does not indicate a bug in the JDK.

转载于:https://my.oschina.net/kepler/blog/205504

HashMap--并发下死循环(HashMap不能在多线程下使用)相关推荐

  1. 分析多线程下jdk1.8之前hashmap的put方法造成死循环而jdk1.8之后如何解决这个死循环

    美团技术博客 图解HashMap(一) HashMap多线程死循环问题(调试查看) HashMap中傻傻分不清楚的那些概念 Java的HashMap是非线程安全的,在多线程下应该使用Concurren ...

  2. 多线程下HashMap的死循环

    https://blog.csdn.net/dingjianmin/article/details/79780350 Java的HashMap是非线程安全的.多线程下应该用ConcurrentHash ...

  3. 多线程/高并发下的HashMap

    有一定经验的开发者都知道,HashMap是非线程安全的. 在高并发环境下,HashMap可能出现致命问题. 一.Rehash是HashMap在扩容时候的一个步骤 HashMap的[容量]是有限的,当经 ...

  4. Java基础:详解HashMap在多线程下不安全

    今天想知道HashMap为什么在多线程下不安全,找了许多资料,终于理解了. 首先先了解一下HashMap: HashMap实现的原理是:数组+链表 HashMap的size大于等于(容量*加载因子)的 ...

  5. HashMap为什么在多线程下会让cpu100%

    首先HashMap并不是sun公司多线程提供的集合,很多时候我们的程序是一个主线程,用了hashmap并没有什么问题,但是在多线程下会出现问题. hashmap是一个哈希表,存储的数据结构也可以是一个 ...

  6. 为什么HashMap会产生死循环?

    作者:磊哥 来源 | Java面试真题解析(ID:aimianshi666) 转载请联系授权(微信ID:GG_Stone) 面试合集:https://gitee.com/mydb/interview ...

  7. java+线程安全的hash,多线程下HashMap安全问题-ConcurrentHashMap解析

    Java1.5 引入了 java.util.concurrent 包,其中 Collection 类的实现允许在运行过程中修改集合对象.实际上, Java 的集合框架是[迭代器设计模式]的一个很好的实 ...

  8. hashmap扩容时死循环问题

    废话不多说,大家都知道,hashmap不能用于多线程场景中,多线程下推荐使用concurrentHashmap! 但为什么多线程下不能使用hashmap那,主要原因就在于其的扩容机制. 文章是综合他人 ...

  9. HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别

    Hash算法 Hash,一般翻译做"散列",也有直接音译为"哈希"的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的 ...

  10. HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别(转)

    HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别 文章来源:http://www.cnblogs.com/beatIteWeNerverGiveU ...

最新文章

  1. 用css3做一个求婚小动画
  2. Nginx: error while loading shared libraries: libpcre.so.1解决
  3. Android之安装开发环境
  4. JavaWeb之JSP技术总结
  5. 面对10倍需求只用 40% 成本,这是一种怎样的体验?
  6. 【图解】QT 布局、 sizeHint和SizePolicy概念
  7. Linux自学day2
  8. Kaspersky Security Center部署
  9. 台式计算机有线无线网卡设置,笔记本/台式电脑有线网络转无线wifi教程
  10. Hanlp词性对照表 中文词性对照表 英文词性对照表_CodingPark编程公园
  11. Guarded Suspension模式
  12. SpringCloud Zuul配置
  13. 初探 ModBus4j -简单使用指南
  14. 如何从网页获取原图片
  15. DOS命令不需格式化U盘-FAT32轻松转换成NTFS
  16. 教师资格证面试 计算机应用,请问下,中职类教师资格证,科目是计算机应用。可..._教师招聘考试_帮考网...
  17. php设计鸡兔同笼问题解法,鸡兔同笼问题4种解题方法
  18. Go语言学习14-基本流程控制
  19. 【论文精读】AVP-Loc: Surround View Localization and Relocalization Based on HD VectorMap for AVP
  20. Redis系列---集群模式

热门文章

  1. 那些必须要知道的Javascript
  2. Android APK反编译详解(转)
  3. VScode编辑同步Markdown文档到印象笔记
  4. cad图纸怎么看懂_CAD图纸太大,打开的时候很卡怎么办?教你如何给图纸瘦身
  5. qdebug 调试 输出乱码_Python里三个最高逼格的调试神器
  6. vba有下拉框的模糊查找_巧用数据验证制作模糊匹配的下拉列表
  7. python基础作业_017--python基础作业
  8. python逐行输出_python逐行输出
  9. android token机制_Android之window机制token验证
  10. 深度卷积生成对抗网络(DCGAN)原理与实现(采用Tensorflow2.x)