首页 > 编程语言 >Java HashMap为什么线程不安全

Java HashMap为什么线程不安全

时间:2023-03-20 16:23:25浏览次数:52  
标签:JDK1.8 Java HashMap next 插入 线程 死循环

一、学习目标

1、HashMap线程不安全原因:

原因:

  • JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
  • JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

改善:

  • 数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

2、HashMap线程不安全的体现:

  • JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
  • JDK1.8 HashMap线程不安全体现在:数据覆盖

二、HashMap线程不安全、死循环、数据丢失、数据覆盖的原因

1、JDK1.7 扩容引发的线程不安全

HashMap的线程不安全主要是发生在扩容函数中,其中调用了JDK1.7 HshMap#transfer():

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];
            newTable[i] = e;
            e = next;
        }
    }
}

这段代码是HashMap的扩容操作,重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。理解了头插法后再继续往下看是如何造成死循环以及数据丢失的。

2、扩容造成死循环和数据丢失

假设现在有两个线程A、B同时对下面这个HashMap进行扩容操作:

image

正常扩容后的结果是下面这样的:

image

但是当线程A执行到上面transfer函数的第11行代码时,CPU时间片耗尽,线程A被挂起。即如下图中位置所示:

image

此时线程A中:e=3、next=7、e.next=null

image

当线程A的时间片耗尽后,CPU开始执行线程B,并在线程B中成功的完成了数据迁移

image

重点来了,根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中newTable和table都是最新的,也就是说:7.next=3、3.next=null。

随后线程A获得CPU时间片继续执行newTable[i] = e,将3放入新数组对应的位置,执行完此轮循环后线程A的情况如下:

image

接着继续执行下一轮循环,此时e=7,从主内存中读取e.next时发现主内存中7.next=3,此时next=3,并将7采用头插法的方式放入新数组中,并继续执行完此轮循环,结果如下:

image

此时没任何问题。

上轮next=3,e=3,执行下一次循环可以发现,3.next=null,所以此轮循环将会是最后一轮循环。

接下来当执行完e.next=newTable[i]即3.next=7后,3和7之间就相互连接了,当执行完newTable[i]=e后,3被头插法重新插入到链表中,执行结果如下图所示:

image

上面说了此时e.next=null即next=null,当执行完e=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。

并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。

3、JDK1.8中的线程不安全

上面的扩容造成的数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

为什么说 JDK1.8会出现数据覆盖的情况? 我们来看一下下面这段JDK1.8中的put操作代码:

image

其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

除此之前,还有就是代码的第38行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

三、如何使HashMap在多线程情况下进行线程安全操作?

使用 Collections.synchronizedMap(map),包装成同步Map,原理就是在HashMap的所有方法上synchronized。

例如:Collections.SynchronizedMap#get()

public V get(Object key) {
    synchronized (mutex) {
        return m.get(key);
    }
}

四、总结

1、HashMap线程不安全原因:

原因:

  • JDK1.7 中,由于多线程对HashMap进行扩容,调用了HashMap#transfer(),具体原因:某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。
  • JDK1.8 中,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),具体原因:假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

改善:

  • 数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果你去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。

2、HashMap线程不安全的体现:

  • JDK1.7 HashMap线程不安全体现在:死循环、数据丢失
  • JDK1.8 HashMap线程不安全体现在:数据覆盖

五、参考

链接:https://juejin.cn/post/6917526751199526920

标签:JDK1.8,Java,HashMap,next,插入,线程,死循环
From: https://www.cnblogs.com/sunny226/p/17236736.html

相关文章

  • Java敏捷开发框架 高效搭建数据报表
    随着数字化转型在企业中不断深入推动,对企业的财务领域的转型也带来巨大的冲击。外部市场环境的快速变化,使企业经营者的决策时间从原来的几年和几个月缩短到几周甚至几天,组......
  • java常用的工具类之CollectionUtils
    org.apache.commons.collections4.CollectionUtils1.判断集合是否为空或者不为空//为空CollectionUtils.isEmpty(null):trueCollectionUtils.isEmpty(newArrayList(......
  • wpf不在主线程的话放到主线程执行
    ///<summary>///刷新页面状态///</summary>///<paramname="overViewState"></param>///<paramname="strValue"></param>......
  • JAVA并发
    并发三要素:可见性:一个线程对共享变量的修改,另一个线程能马上看到(CPU缓存引起)原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。......
  • java方法-定义及调用
    方法的定义及调用定义:Java的方法类似于其它语言的函数,是一段用来完成特定功能的代码片段,一般情况下,定义一个方法包含以下语法:方法包含一个方法头和一个方法体,下面是......
  • java网络编程
    初始网络编程常见浏览器的架构有些比较大型的软件这2种架构都会兼顾2种架构的优缺点比较B/S架构,以网页游戏为例画面烂C/S架构在c/s架构中,安装包中已经有......
  • java-线程之间的协作
      一、线程之间的协作:join()classJoinExample{privateclassAextendsThread{@Overridepublicvoidrun(){System.out.pri......
  • java 根据word xml模板生成word(个人v2版本)
    这里用的是poi相关jar包以及freemarker插值技术实现,poi相关jar包这里不再述说1,编辑word并保存为xml其中需要动态输出的内容使用${xxx}代替,xxx是你的java类属性值,如:年龄:${age......
  • 尚硅谷_宋红康_第10章_多线程
    第10章_多线程讲师:尚硅谷-宋红康(江湖人称:康师傅)官网:http://www.atguigu.com本章专题与脉络我们之前学习的程序在没有跳转语句的情况下,都是由上至下沿着一条路径依......
  • Java线程知识点总结
    文章目录​​Java线程基础​​​​线程简介​​​​什么是进程​​​​什么是线程​​​​进程和线程的区别​​​​创建线程​​​​Thread​​​​Runnable​​​​Calla......