事故背景

一个CPU使用率飙升至100%的线上故障,原因是在并发情况下使用HashMap导致死循环。
当cpu使用率100%时,查看堆栈,发现程序都卡在了HashMap.get()这个方法上了,重启程序后问题消失。但是过段时间又会来。

HashMap结构

HashMap 是我们经常会用到的集合类,JDK 1.7 之前底层使用了数组加链表的组合结构,如下图所示:

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,这个成本相当的大。


JDK 1.7 HashMap的rehash源代码

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

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;
}

检查容量是否超标

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,如果超过,需要resizeif (size++ >= threshold)resize(2 * table.length);
}

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

void resize(int newCapacity)
{Entry[] oldTable = table;int oldCapacity = oldTable.length;......//创建一个新的Hash TableEntry[] newTable = new Entry[newCapacity];//将Old Hash Table上的数据迁移到New Hash Table上transfer(newTable);table = newTable;threshold = (int)(newCapacity * loadFactor);
}

迁移的源代码:

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);}}
}

旧数组元素迁移到新数组时,依旧采用头插入法,这样将会导致新链表元素的逆序排序。

多线程并发扩容的情况下,链表可能形成死链(环形链表)。一旦有任何查找元素的动作,线程将会陷入死循环,从而引发 CPU 使用率飙升。


JDK1.8 改进方案

JDK1.8 HashMap 底层结构进行彻底重构,使用数组链表/红黑树方式这种组合结构。


新元素依旧通过取模方式获取 Table 数组位置,然后再将元素加入链表尾部。一旦链表元素数量超过 8 之后,自动转为红黑树,进一步提高了查找效率。

由于 JDK1.8 链表采用尾插入法,从而避免并发扩容情况下链表形成死链的可能。

虽然JDK1.8能避免并发扩容情况下的死链问题,但是HashMap仍然不适合用于并发场景。(并发赋值时被覆盖size 计算问题


ConcurrentHashMap

JDK1.7 ConcurrentHashMap结构

JDK1.7 ConcurrentHashMap 数据结构如下所示:

Segament 是一个ConcurrentHashMap内部类,底层结构与 HashMap 一致。另外Segament 继承自 ReentrantLock

当新元素加入 ConcurrentHashMap 时,首先根据 key hash 值找到相应的 Segament。接着直接对 Segament 上锁,若获取成功,后续操作步骤如同 HashMap

由于锁的存在,Segament 内部操作都是并发安全,同时由于其他 Segament 未被占用,因此可以支持 concurrencyLevel 个线程安全的并发读写。

size 统计问题

虽然 ConcurrentHashMap 引入分段锁解决多线程并发的问题,但是同时引入新的复杂度,导致计算 ConcurrentHashMap 元素数量将会变得复杂。

由于 ConcurrentHashMap 元素实际分布在 Segament 中,为了统计实际数量,只能遍历 Segament数组求和。

为了数据的准确性,这个过程过我们需要锁住所有的 Segament,计算结束之后,再依次解锁。不过这样做,将会导致写操作被阻塞,一定程度降低 ConcurrentHashMap性能。
所以这里对 ConcurrentHashMap#size 统计方法进行一定的优化。

Segment 每次被修改(写入,删除),都会对 modCount(更新次数)加 1。只要相邻两次计算获取所有的 Segment modCount 总和一致,则代表两次计算过程并无写入或删除,可以直接返回统计数量。
如果三次计算结果都不一致,那没办法只能对所有 Segment 加锁,重新计算结果。
这里需要注意的是,这里求得 size 数量不能做到 100% 准确。这是因为最后依次对 Segment 解锁后,可能会有其他线程进入写入操作。这样就导致返回时的数量与实际数不一致。
不过这也能被接受,总不能因为为了统计元素停止所有元素的写入操作。

性能问题

想象一种极端情况的,所有写入都落在同一个 Segment中,这就导致ConcurrentHashMap 退化成 SynchronizedMap,共同抢一把锁。

JDK1.8 改进方案

JDK1.8 之后,ConcurrentHashMap 取消了分段锁的设计,进一步减锁冲突的发生。另外也引入红黑树的结构,进一步提高查找效率。

数据结构如下所示:


Table 数组的中每一个 Node 我们都可以看做一把锁,这就避免了 Segament 退化问题。

另外一旦 ConcurrentHashMap 扩容, Table 数组元素变多,锁的数量也会变多,并发度也会提高。

JDK1.8 使用 CAS 方法加 synchronized 方式,保证并发安全。


总结

  • HashMap 在多线程并发的过程中存在死链与丢失数据的可能,不适合用于多线程并发使用的场景的,我们可以在方法的局部变量中使用。

  • SynchronizedMap 虽然线程安全,但是由于锁粒度太大,导致性能太低,所以也不太适合在多线程使用。

  • ConcurrentHashMap 由于使用多把锁,充分降低多线程并发竞争的概率,提高了并发度,非常适合在多线程中使用。ConcurrentHashMap 分段锁的经典思想,我们可以应用在热点更新的场景,提高更新效率。

原文转载自:https://blog.csdn.net/qq_33404395/article/details/105233302

HashMap引发死链问题(HashMap、ConcurrentHashMap原理解析)相关推荐

  1. 【金融科技前沿】区块链和数字经济(区块链的缘起,现状和发展、区块链技术架构和原理解析 、区块链应用和案例综述)

    1.缘由 前段时间微众银行的区块链首席架构师张开翔老师给我们上了一堂近三小时的课,与其说是在上课,不如说是一场思想的享受.开翔老师跟我们学院联系挺密切的,之前比赛也见过他,我印象中他一直是一位顶级的技 ...

  2. 详述 JDK1.7 中 HashMap 会发生死链的原因

    文章目录 前置知识 死循环执行步骤1 死循环执行步骤2 死循环执行步骤3 解决方案 总结 前置知识 HashMap死循环是一个比较常见.比较经典的问题,在日常的面试中出现的频率比较高,所以接下来咱们通 ...

  3. ConcurrentHashMap 原理解析

    了解ConcurrentHashMap 实现原理,建议首先了解下HashMap实现原理. HashMap 源码解析(JDK1.8) 为什么要用ConcurrentHashMap     HashMap ...

  4. Java并发编程之ConcurrentHashMap原理解析

    ConcurrentHashMap get: /*** 根据键值key获取value,根据key.equals方法判断两个元素是否相同* @param key 键* @return 如果key存在则返 ...

  5. Java集合篇:HashMap 与 ConcurrentHashMap 原理总结

    一.HashMap原理总结: 1.什么是HashMap: (1)HashMap 是基于 Map 接口的非同步实现,线程不安全,是为了快速存取而设计的:它采用 key-value 键值对的形式存放元素( ...

  6. 【死链】JDK1.7中HashMap在多线程环境的并发问题源码分析

    文章目录 一.HashMap在JDK1.7中的并发问题 二.死链如何产生? 三.如何解决HashMap并发问题 参考文献 一.HashMap在JDK1.7中的并发问题 在JDK1.7中的HashMap ...

  7. Java中的集合类——HashMap中的并发死链

    Java中的集合类--HashMap中的并发死链 ReHash过程 正常的ReHash过程: 并发的Rehash过程 解决办法 ReHash过程 正常的ReHash过程: 并发的Rehash过程 (1 ...

  8. Java常见问题之HashMap扩容、树化、死链

    写在前边 HashMap属于比较常用的数据结构了,面试过程中也经常会被问到,本篇就知识点,展开问答式分析,重点聊聊hash冲突.扩容死链.容量为2的n次方.1.7和1.8之间的区别等问题~ 如何解决H ...

  9. [Java]JDK1.7中HashMap的并发死链

    [Java]JDK1.7中HashMap的并发死链 HashMap的并发死链现象发生在扩容时,在扩容过程中**transfer()**方法负责把旧的键值对转移到新的表中,其代码如下: void tra ...

最新文章

  1. 模拟linux磁盘满,linux 磁盘满了简单处理一下
  2. iOS 新窗口在最上层
  3. iofd:文件描述符_文字很重要:谈论设计时18个有意义的描述符
  4. Python的看门狗实现自动化实时对服务器、Windows或Linux文件夹的实时监控
  5. c++ list遍历_Creo二次开发 文件循环遍历
  6. 二维码软件如何扫描二维码打开网页
  7. WPF帝友借贷系统MV模式开发日志2021/04/04_注册验证邮箱验证码发送
  8. 网络运维工程师面试题分享
  9. 截取字符串中某一段数据
  10. 三个div怎么分别靠左、居中、靠右显示
  11. iOS开发中屏幕旋转(二)
  12. windows许可证即将过期 win10的解决办法
  13. vue.js--v-show隐藏图片
  14. python列表替换元素_24_Pandas.DataFrame,Series元素值的替换(replace)
  15. 数仓、数据湖、湖仓一体、数据网格的探索与研究
  16. 地方征信平台第2讲:河北省征信
  17. Linux 多个留后门姿势
  18. LinuxProbe学习笔记(十一)
  19. 数据库插入数据报错Unknown error finalizing or resetting statement (19: UNIQUE constraint failed:xxx.xxx)
  20. 必应缤纷桌面产品分析 马骏

热门文章

  1. C++ Using 用法
  2. STM32 高级定时器周期、频率、占空比、对外输出电压详解
  3. nth-child 与 nth-of-type
  4. 用ps扣出透明背景图片,做图标的方法
  5. 17. 06-图3 六度空间 数据结构 浙江大学 拼题
  6. 提高信心的十个方法,助你考研坚持到底!
  7. 桌面不显示IE浏览器图标
  8. R语言ggplot2可视化:使用geom_step可视化阶梯图、阶梯线图、可视化分组阶梯线图
  9. HTTP和TCP之间的关系
  10. Win10 Chinese输入法修复/note