zoukankan      html  css  js  c++  java
  • 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 中的方法进行加锁处理,保证多线程下操作安全!

  • 相关阅读:
    SQL对Xml字段的操作
    T-SQL查询进阶-10分钟理解游标
    如何完成支付宝的支付功能?
    支付宝api指南
    DDD学习笔录——提炼问题域之知识提炼与协作
    DDD学习笔录——领域驱动设计的常见误区(即错误的理解)
    DDD学习笔录——简介领域驱动设计的实践与原则
    DDD学习笔录——简介DDD的战术模式、问题空间和解空间
    DDD学习笔录——简介DDD的战略模式如何塑造应用程序的架构
    初识DDD
  • 原文地址:https://www.cnblogs.com/takemyjavalisfe/p/14367243.html
Copyright © 2011-2022 走看看