浅谈HashMap在高并发下的问题

 更新时间:2021年07月31日 12:01:07   作者:梧桐和风  
这篇文章主要介绍了HashMap在高并发下的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

前言

总所周知,HashMap不是线程安全的,在高并发情况下会出现问题。特别是,在java1.7中,多线程的HashMap会出现CPU 100%的严重问题。这个问题是怎样产生的,后续版本还会有这个问题吗(指java8及后续版本)?下面就来用通俗的语言讲解下。

解析

关于这个问题,是由于java7多线程扩容机制下链表变为循环链表,再获取该链表导致的。

看下java7中扩容的代码。java7中HashMap的实现为数组+链表的形式,没有红黑树。

java7扩容的原则很简单,新数组长度为原数组2倍。遍历原数组,将数组每个位置(有可能为空,有可能只有一个数组,有可能是一个链表)重新哈希,放到对应的新数组上。全部遍历完后更改数组指针,指向新数组。需要注意的是,这里重哈希将链表元素放到新数组,使用的是头插法。

 // 扩容核心方法,基本思想就是遍历数据,使用头插法将旧数组元素移到新数组。
 void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        // 遍历旧数组
        for (Entry<K,V> e : table) {
            // 元素不为空。遍历该位置链表
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i]; // 头插法,新节点next指向该位置首节点
                newTable[i] = e; // 新元素归位
                e = next; // 指向下一个节点,继续遍历
            }
        }
    }
   void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        Entry[] newTable = new Entry[newCapacity]; // 创建新数组
        transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 扩容
        table = newTable;  // 更改指针
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

这里处理的话,如果单线程情况下不会有问题。如果在多线程情况下,会导致链表在扩容过程中形成循环链表。

形成循环链表的原因在于多线程和头插法。试想,两个线程在添加元素时,同时发现该扩容了,然后同时发起扩容过程。由上述代码可知,扩容完成之前是在自己的线程里创建一个新数组。等扩容完成后(也就是将原数组元素迁移到新数组后)再更改指针指向新扩容数组。

举例初始HashMap是这样的

在这里插入图片描述

假设两个线程同时扩容,一个线程扩容到一半后被挂起。(标识了某链表的e和next),另一个线程执行扩容,且完成了扩容。

红色的数组和元素表示线程1,也就是扩容一半挂起的线程,而线程二已完成扩容。观察完成扩容的线程二,在3的位置,该链表的位置顺序已经改变(原数组顺讯为3->7,现在反过来了,这是使用头插法的效果,你也可以对着代码试试)。从图中也可以看出,线程1,2分别创建了自己的新数组,并在自己的新数组中完成扩容。

这时线程1开始执行。熟悉下它即将执行的代码。

       // transfer 方法循环部分
         while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i]; // 头插法,新节点next指向该位置首节点
                newTable[i] = e; // 新元素归位
                e = next; // 指向下一个节点,继续遍历
            }

下面线程1将使用头插法将元素插入线程1新建的数组中去。注意此时e指向的是Key3,next指向的是Key7。不用想也知道后面操作会有问题。因为现在的next指针指的不是e的下一个元素,而是它的前一个元素!

如果继续走代码的话,把Key3(当前e指向元素)放入新数组后,再把Key7放入新数组,后面会放哪个元素?当然又是Key3了,因为Key7next是Key3,这样就形成了死循环。

在这里插入图片描述

java8的改进

  • 添加了红黑树,当链表长度大于8时,会将链表转为红黑树。
  • 扩容后,新数组中的链表顺序依然与旧数组中的链表顺序保持一致。具体JDK8是用 head 和 tail 来保证链表的顺序和之前一样,这样就不会产生循环引用。也就没有死循环了。
  • 虽然修复了死循环的BUG,但是HashMap 还是非线程安全类,仍然会产生数据丢失等问题。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java多线程批量拆分List导入数据库的实现过程

    java多线程批量拆分List导入数据库的实现过程

    这篇文章主要给大家介绍了关于java多线程批量拆分List导入数据库的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-10-10
  • spring boot集成mongodb的增删改查的示例代码

    spring boot集成mongodb的增删改查的示例代码

    这篇文章主要介绍了spring boot集成mongodb的增删改查的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • 关于rocketmq中日志文件路径的配置指南

    关于rocketmq中日志文件路径的配置指南

    rocketmq是java编写的,也就是可以指定启动参数,下面这篇文章主要给大家介绍了关于rocketmq中日志文件路径的配置指南,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-04-04
  • SpringBoot整合minio快速入门教程(代码示例)

    SpringBoot整合minio快速入门教程(代码示例)

    这篇文章主要介绍了SpringBoot整合minio快速入门实现文件上传和下载的示例代码,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • Mybatis insert方法主键回填和自定义操作

    Mybatis insert方法主键回填和自定义操作

    这篇文章主要介绍了Mybatis insert方法主键回填和自定义操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • SpringBoot MDC全链路调用日志跟踪实现详解

    SpringBoot MDC全链路调用日志跟踪实现详解

    这篇文章主要为大家介绍了SpringBoot MDC全链路调用日志跟踪实现详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • maven环境变量配置讲解

    maven环境变量配置讲解

    这篇文章主要介绍了maven环境变量配置讲解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • Java仿12306图片验证码

    Java仿12306图片验证码

    这篇文章主要为大家详细介绍了Java仿12306的图片验证码的相关资料,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-04-04
  • 使用springboot+druid双数据源动态配置操作

    使用springboot+druid双数据源动态配置操作

    这篇文章主要介绍了使用springboot+druid双数据源动态配置的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • 模仿J2EE的session机制的App后端会话信息管理实例

    模仿J2EE的session机制的App后端会话信息管理实例

    下面小编就为大家分享一篇模仿J2EE的session机制的App后端会话信息管理实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-11-11

最新评论