• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
没心情
Beauty begins the moment you decide to be yourself.
博客园    首页    新随笔    联系   管理    订阅  订阅
HashMap在多线程环境下操作可能会导致程序死循环

01、问题描述
经常有些面试官会问,是否了解过 HashMap 在多线程环境下使用时可能会发生死循环,导致服务器 cpu 100% 的线上故障?

关于这个问题,原因竟是多线程环境下使用 HashMap 造成的死循环,并且这个事发生了很多次。虽然 Java 官方明确表示,在多线程环境下不推荐使用 HashMap。

为什么会产生死循环呢?下面我们来还原一下问题的经过。

02、问题重现

注意注意,小编在进行测试的时候,使用的是 JDK1.7 的版本!

如果你使用 JDK1.8 的版本,不好意思,不一定能复现这个问题!因为 JDK1.8 已经修复了这个问题,但是依然不建议在多线程环境下使用 HashMap!

我们继续来看看为什么使用 JDK1.7 会出现这个问题!

整个 put 过程,大致可以分如下几个步骤:

第一步是通过 key 计算出来的 hash 和 equals 来判断元素是否存在,如果存在,直接覆盖;反之,插入;

第二步是将元素插入到 hash 表中,如果不同的元素都在一个 hash 数组下标下,就以链表的形式,采用头插法存储在 hash 节点下;

最后就是判断当前数组容量是否大于扩容阀值,如果大于,就进行扩容处理,然后将旧元素复制到新的数组中;

好了,这个过程基本上没啥问题。

我们再来说明一下扩容中重新计算元素 hash 的过程!
假设在单线程环境下,我们初始化的时候,给定的数组容量是2,分别添加3个元素,内容如下:

key=3,value=A;

key=4,value=B;

key=5,value=C;

单线程下扩容元素 hash 过程

假设在单线程环境下,我们初始化的时候,给定的数组容量是2,分别添加3个元素,添加完成之后,数组就会进行扩容处理,扩容后 hash 的容量为原来的2倍,单线程环境下,一切看起来都很正常,扩容过程也相当顺利。

多线程扩容元素 hash 过程

假设我们有两个线程,来分别添加3个元素。同样的三个元素:

第一次循环过程如下:

第1步:此时 e 等于{key:3,value:A},next=e.next={key:5,value:C};

第2步:通过 key 重新 hash 计算得到下标 i = 3;

第3步:newTable为局部变量,内容都为null,所以 e.next = newTable[i]=null;

第4步:newTable[i]=e={key:3,value:A};

第5步:e=next={key:5,value:C};

循环结果如下,e={key:5,value:C},满足while()循环条件,接着继续!
第二次循环过程如下:

第1步:此时 e 等于{key:5,value:C},取最新的链表结构,next=e.next={key:3,value:A};

第2步:通过 key 重新 hash 计算得到下标 i = 3;

第3步:在第一次循环中,newTable[i]已经插入值,所以 e.next = newTable[i]={key:3,value:A};

第4步:newTable[i]=e={key:5,value:C};

第5步:e=next={key:3,value:A};

循环结果如下,e={key:3,value:A},满足while()循环条件,接着继续!
第1步:此时 e 等于{key:3,value:A},取最新的链表结构,next=e.next=null;

第2步:通过 key 重新 hash 计算得到下标 i = 3;

第3步:在第二次循环中,newTable[i]已经插入值,所以 e.next = newTable[i]={key:5,value:C};

第4步:newTable[i]=e={key:3,value:A};

第5步:e=next=null;

循环结果如下,e=null,while()程序不在循环!

线程二执行完添加任务之后,在准备将旧元素迁移到新元素的时候,也就是准备 rehash 时,突然被 CPU 挂起,此时阻塞不再往下执行!而线程一继续执行直到扩容完成。
总的来说

   起因主要是hashmap在put数据时,超过预设长度则会自动扩容,即resize方法,而引起死锁的核心逻辑为resize中的transfer方法
> JDK1.8之前,为了提高rehash的速度,冲突链表是使用头插法,因为头插法是操作速度最快的,找到数组位置就直接找到插入位置了,头插法在多线程下回引起死循环
> JDK1.8之后开始加入红黑树,当链表长度大于8时链表就会转换成红黑树,这样就大大提高了在冲突链表查找的速度,同时因为链表的长度不可能大于8,链表在rehash的消耗就小很多,所以JDK1.8使用尾插法也避免了死循环问题``

所以,不建议在多线程环境下使用 HashMap,那如果要在多线程环境下使用 map 操作类,该怎么办呢?

04、解决办法
办法肯定是有的,如果大家想在多线程场景下使用 HashMap,有两种解决办法:

第一种,推荐使用并发包中的 ConcurrentHashMap 类,一种使用分段锁的 hashMap 类

另一种,是使用Collections.synchronizedMap(Mao<K,V> map)工具方法,将 HashMap 变成一个线程安全的 map,其实就是对 map 中的方法进行加锁处理,保证多线程下操作安全!

posted on 2021-02-03 15:21  No-心情  阅读(978)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3