前言
几个月前,上线了一个版本。但是上线了几个小时之后 CPU 突然暴增至99%,在网上搜了一下,多半是因为出现死循环问题了。就用 jstack dump 了当时的线程快照,发现这次死循环问题的起源是 HashMap 的 get()方法。之后先是迅速重启了服务,这样可以让服务先运行一段时间。然后立即修复了这个 bug并提交到 SVN。

这次事故的原因是因为开发时没有注意到 HashMap 是非线程安全的,而使用 HashMap 的那个地方又是 PV 级别的代码,多线程并发非常容易出现问题。但因为这块代码不是我开发的,我也不清楚具体的细节,就没有过多关注。最近正好在看 HashMap 的源码,突然想起来这事,就正好看看究竟是神马原因造成了 HashMap 的死锁问题。

一、HashMap 的底层实现
这个可以参考上一篇文章:HashMap 源码剖析,具体介绍了 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 表的尺寸和容量非常的重要。一般来说,Hash 表这个容器当有数据要插入时,都会检查容量有没有超过设定的 thredhold,如果超过,需要增大 Hash 表的尺寸,但是这样一来,整个 Hash 表里的无素都需要被重算一遍。这叫 rehash,这个成本相当的大。

二、源码剖析
首先来猜下,神马情况会造成死锁呢?

我们知道,如果要造成死循环,肯定和链表链表有关,因为只有链表才有指针。但是在源码剖析中我们知道,每次添加元素都是在链表头部添加元素,怎么会造成死锁呢?

其实,关键就在于rehash过程。在前面我们说了是 HashMap 的get()方法造成的死锁。既然是 get()造成的死锁,一定是跟put()进去元素的位置有关,所以我们从 put()方法开始看起。

 1 public V put(K key, V value) {2         if (table == EMPTY_TABLE) {3             inflateTable(threshold);4         }5         if (key == null)6             return putForNullKey(value);7         int hash = hash(key);8         int i = indexFor(hash, table.length);9         //如果该 key 存在,就替换旧值
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         //如果没有这个 key,就插入一个新元素!跟进去看看
22         addEntry(hash, key, value, i);
23         return null;
24     }
25
26     void addEntry(int hash, K key, V value, int bucketIndex) {
27      //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
28         if ((size >= threshold) && (null != table[bucketIndex])) {
29             resize(2 * table.length);
30             hash = (null != key) ? hash(key) : 0;
31             bucketIndex = indexFor(hash, table.length);
32         }
33
34         createEntry(hash, key, value, bucketIndex);
35     }
36
37     //新建一个更大尺寸的hash表,把数据从老的Hash表中迁移到新的Hash表中。
38     void resize(int newCapacity) {
39         Entry[] oldTable = table;
40         int oldCapacity = oldTable.length;
41         if (oldCapacity == MAXIMUM_CAPACITY) {
42             threshold = Integer.MAX_VALUE;
43             return;
44         }
45
46         //创建一个新的 Hash 表
47         Entry[] newTable = new Entry[newCapacity];
48         //转移!!!!跟进去
49         transfer(newTable, initHashSeedAsNeeded(newCapacity));
50         table = newTable;
51         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
52     }
53
54     //高能预警!!!!重点全在这个函数中
55     void transfer(Entry[] newTable, boolean rehash) {
56         int newCapacity = newTable.length;
57         for (Entry<K,V> e : table) {
58             while(null != e) {
59                 Entry<K,V> next = e.next;
60                 if (rehash) {
61                     e.hash = null == e.key ? 0 : hash(e.key);
62                 }
63                 int i = indexFor(e.hash, newCapacity);
64                 e.next = newTable[i];
65                 newTable[i] = e;
66                 e = next;
67             }
68         }
69     }

看到最后这个函数transfer(),就算到达了问题的关键。我们先大概看下它的意思:

  1. 对索引数组中的元素遍历
  2. 对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,因为可能有元素,所以先将 e.next 指向新 Hash 表的第一个元素(如果是第一次就是 null),这时候新 Hash 的第一个元素是 e,但是 Hash 指向的却是 e 没转移时候的第一个,所以需要将 Hash 表的第一个元素指向 e
  3. 循环2,直到链表节点全部转移
  4. 循环1,直到所有索引数组全部转移
    经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以,HashMap 的死锁问题就出在这个transfer()函数上

三、单线程 rehash 详细演示
单线程情况下,rehash 不会出现任何问题:

假设hash算法就是最简单的 key mod table.length(也就是数组的长度)。
最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以后碰撞发生在 table[1]接下来的三个步骤是 Hash表 resize 到4,并将所有的 key,value 重新rehash到新 Hash 表的过程

四、多线程 rehash 详细演示
首先我们把关键代码贴出来,如果在演示过程中忘了该执行哪一步,就退回来看看:

 1 while(null != e) {2     Entry<K,V> next = e.next;3     if (rehash) {4         e.hash = null == e.key ? 0 : hash(e.key);5     }6     int i = indexFor(e.hash, newCapacity);7     e.next = newTable[i];8     newTable[i] = e;9     e = next;
10 }

上面代码就是重中之重,不过我们可以再简化一下,因为中间的 i 就是判断新表的位置,我们可以跳过。简化后代码:

1 while(null != e) {
2     Entry<K,V> next = e.next;
3     e.next = newTable[i];
4     newTable[i] = e;
5     e = next;
6 }

去掉了一些与本过程冗余的代码,意思就非常清晰了:

Entry<K,V> next = e.next;

——因为是单链表,如果要转移头指针,一定要保存下一个结点,不然转移后链表就丢了

e.next = newTable[i];
  • ——e 要插入到链表的头部,所以要先用 e.next 指向新的 Hash 表第一个元素(为什么不加到新链表最后?因为复杂度是 O(N))
newTable[i] = e;

——现在新 Hash 表的头指针仍然指向 e 没转移前的第一个元素,所以需要将新 Hash 表的头指针指向 e

e = next

——转移 e 的下一个结点
好了,代码层面已经全部 ok,下面开始演示:

假设这里有两个线程同时执行了put()操作,并进入了transfer()环节
粉红色代表线程1,浅蓝色代码线程2
1. 初始状态
现在假设线程1的工作情况如下代码所示,而线程2完成了整个transfer()过程,所以就完成了 rehash。

1 while(null != e) {
2     Entry<K,V> next = e.next; //线程1执行到这里被调度挂起了
3     e.next = newTable[i];
4     newTable[i] = e;
5     e = next;
6 }

那么现在的状态为:

从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 rehash 后,就指向了线程2 rehash 后的链表。

  1. 第一步
    然后线程1被唤醒了:

  2. 执行e.next = newTable[i],于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以e.next = null,

  3. 执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)
    第二步
    然后该执行 key(3)的 next 节点 key(7)了:

  5. 现在的 e 节点是 key(7),首先执行Entry<K,V> next = e.next ,那么 next 就是 key(3)了

  6. 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
  7. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7)
  8. 执行e = next,将 e 指向 next,所以新的 e 是 key(3

  9. 第三步
    然后又该执行 key(7)的 next 节点 key(3)了:

  10. 现在的 e 节点是 key(3),首先执行Entry<K,V> next = e.next,那么 next 就是 null

  11. 执行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
  12. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)
  13. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)
    这时候的状态如图所示:

很明显,环形链表出现了!!当然,现在还没有事情,因为下一个节点是 null,所以transfer()就完成了,等put()的其余过程搞定后,HashMap 的底层实现就是线程1的新 Hash 表了。

没错,put()过程虽然造成了环形链表,但是它没有发生错误。它静静的等待着get()这个冤大头的到来。

  1. 死锁吧,骚年!!!
    现在程序被执行了一个hashMap.get(11),这时候会调用getEntry(),这个函数就是去找对应索引的链表中有没有这个 key。然后。。。。悲剧了。。。Infinite Loop~~

五、启示
通过上面的讲解,我们就弄明白了 HashMap 死锁的原因,其实在很久以前这个 Bug 就被提交给了 Sun,但是 Sun 认为这不是一个 Bug,因为文档中明确说了 HashMap 不是线程安全的。要并发就使用 ConcurrentHashMap。

因为 HashMap 为了性能考虑,没有使用锁机制。所以就是非线程安全的,而 ConcurrentHashMap 使用了锁机制,所以是线程安全的。当然,要知其然知其所以然。最好是去看一下 ConcurrentHashMap 是如何实现锁机制的(其实是分段锁,不然所有的 key 在锁的时候都无法访问)。就像侯捷在《STL 源码剖析》中说的:

源码面前,了无秘密。
对我们的启示在前面的文章踩坑记中就提到过:

使用新类、新函数时,一定一定要过一遍文档
不要望文生义或者凭直觉“猜”,不然坑的不仅仅是自己。

转自:https://blog.csdn.net/Luxia_24/article/details/52344367

关于hashmap的深入-hashmap产生死锁的详解相关推荐

  1. HashMap 是如何工作的?图文详解,一起来看看!

    1 HashMap在JAVA中的怎么工作的? 基于Hash的原理 2 什么是哈希? 最简单形式的 hash,是一种在对任何变量/对象的属性应用任何公式/算法后, 为其分配唯一代码的方法. 一个真正的h ...

  2. 关于上上文hashmap的深入-hashmap产生死锁的详解

    一.HashMap 的底层实现 这个可以参考上一篇文章:HashMap 源码剖析,具体介绍了 HashMap 的底层实现: 数组:充当索引  链表:处理碰撞 简单地说一下: HashMap通常会用一个 ...

  3. 【Java基础】HashMap原理详解

    [Java基础]HashMap原理详解 HashMap的实现 1. 数组 2.线性链表 3.红黑树 3.1概述 3.2性质 4.HashMap扩容死锁 5. BATJ一线大厂技术栈 HashMap的实 ...

  4. HashTable和HashMap的区别详解

    HashTable和HashMap的区别详解 一.HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同 ...

  5. java hashmap 重复_java HashMap插入重复Key值问题

    今天在用到了HashMap来添加数据,发现有重复的key添加.查看java文档终于知道了解决方法.请看下面原来的例子: class User { private String id; private  ...

  6. Java源码详解二:HashMap源码分析--openjdk java 11源码

    文章目录 HashMap.java介绍 1.HashMap的get和put操作平均时间复杂度和最坏时间复杂度 2.为什么链表长度超过8才转换为红黑树 3.红黑树中的节点如何排序 本系列是Java详解, ...

  7. Java源码详解零:HashMap介绍

    文章目录 Java详解(0):HashMap介绍,HashMap的迭代,HashMap的线程安全问题 HashMap介绍 HashMap的迭代 HashMap的线程安全问题 Java详解(0):Has ...

  8. HashMap面试深入详解jdk1.8

    HashMap是Java后端工程师面试的必问题,因为其中的知识点太多,很适合用来考察面试者的Java基础.今天基于jdk1.8来研究一下HashMap的底层实现. HashMap的内部数据结构 JDK ...

  9. HashMap 详解七

    使用 Iterator 遍历 通过 HashMap.entrySet().iterator() 方法获取迭代器, 使用 next 方法对 HashMap 进行遍历. HashMap<String ...

最新文章

  1. 微信小程序自定义组件Component的简单使用
  2. ssh端口映射,本地转发
  3. CV:Visual Studio 2015版本+CUDA8.0+Cudnn8.0+OpenCV 3.1.0版本完美解决的详细攻略
  4. Eclipse console 中文乱码解决
  5. P4292-[WC2010]重建计划【长链剖分,线段树,0/1分数规划】
  6. 自适应滤波实例之噪声抵消
  7. 系统I/O小程序-文件拷贝
  8. 优化JS代码的34种方法(上)
  9. iOS数据持久化(二)SQLite
  10. testbench的简单例子和模板
  11. 2017OKR年终回顾与2018OKR初步规划
  12. 迅捷fw325r虚拟服务器设置,迅捷FAST FW325R路由器无线桥接设置方法
  13. C语言编制排班系统流程图,智能排班系统流程图怎样绘制
  14. mysql 生日排序 查询生日由近到远 按照生日排序
  15. cdn/github_cdn加速配置
  16. 微信小程序 生成小程序码 + Java后台
  17. x264和x265编码技术的区别
  18. 五月集训总结——来自阿光
  19. 法国iut计算机转专业,法国艺术留学能不能够申请转专业.docx
  20. 复习电商笔记-21-linux版主从复制

热门文章

  1. 【EduCoder实训答案】大数据系统及应用-HDFS实训
  2. 大疆无人机安卓Mobile Sdk开发(五)解决M300Rtk H20相机无法获取图片视频的问题
  3. 所谓上拉电阻和下拉电阻
  4. react总结之jsx是什么,jsx语法规则
  5. Spring Boot连接Oracle数据库驱动加载不上的问题(pom.xml引入ojdbc报错的问题)
  6. MATLAB常用指令及解释(持续更新中)
  7. SRM 475 DIV1 900
  8. 【Linux】CentOS7 C#开发环境搭建笔记(Jexus安装、配置、部署)
  9. Python学习:小数/浮点数(float)类型详解
  10. (阿里offer)春招知识点总结1:java基础+集合+并发+jvm+ssm