HashMap是线程安全的吗?为什么呢?

news2025/1/10 15:24:14

 HashMap是线程安全的吗?为什么呢?

HashMap是线程不安全的!

线程不安全体现在JDK.1.7时在多线程的情况下扩容可能会出现死循环或数据丢失的情况,主要是在于扩容的transfer方法采用的头插法,头插法会把链表的顺序给颠倒过来,这是引起死循环的关键。在JDK1.8时在多线程的情况下扩容会可能会出现数据覆盖的情况,例如有两个线程A、B都在进行put操作(对HashMap进行put操作实际上是调用了putVal()方法),并且这两个线程hash函数计算出的插入下标是相同的,当线程A执行完putVal()方法中的一句用于判断有没有发生hash碰撞的代码后(下面源代码的15行),由于时间片耗尽导致被挂起(注意这个时候线程A的判断结果是没有发生hash碰撞,保存了这个判断结果,但是还没有执行下一句插入元素代码,这个时候被挂起了),而线程B得到时间片后也是调用putVal()方法插入元素,由于两个线程hash函数计算出的下标是一样的,并且前面线程A因为时间片到了还没来得及插入元素就被挂起了,所以这个时候线程B判断结果也是没有hash碰撞,直接在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,所以HashMap是线程不安全。

原因:

  • JDK1.7 中HashMap线程不安全体现在多线程扩容导致死循环、数据丢失
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];  //线程A执行完这句后因为时间片耗尽就被挂起了
            newTable[i] = e;
            e = next;
        }
    }
}

HashMap的扩容就是调用上面的transfer方法来实现的,扩容后要采用头插法将元素迁移到新数组中,头插法会将链表的顺序翻转,这也是形成死循环的关键点。

上面的代码主要看下面的四句代码

//重新定义下标
Entry<K,V> next = e.next;
//下面三句就是头插法的实现
 e.next = newTable[i];
 newTable[i] = e;
 e = next;

模拟扩容造成死循环和数据丢失

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

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

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

解析:线程A中:e=3、next=7、e.next=null,可以看到图1的链表中第一个是3,下一个是7,所以transfer函数第一次执行中e=3,next=7,本来根据图1的情况应该是e.next=7,但是线程A在transfer函数中执行到了 e.next = newTable[i]; 这句,而newTable[i]由于是刚扩容的所以是为null,所以执行这句后 e.next =null

线程A挂起后,此时线程B得到时间片后正常执行,并完成resize扩容操作,结果如下:

线程B完成扩容后,此时主内存中newTable和table都是最新的

也就是说主内存中7.next=3和3.next=null(这里我一开始就疑惑为什么线程B都扩容完了线程A拿到时间片后还要扩容?其实是因为线程A压根就不知道线程B已经扩容完了,线程A也不管你扩容完了没,反正就是继续执行自己之前没有执行完的代码,由于是根据线程B扩容完后存放在内存中的数据继续扩容的,所以线程A才会出现下面的数据丢失和死循环)

随后线程A获得CPU时间片继续执行newTable[i] = e和e = next;这两句代码,线程A在挂起之前的数据是e=3、next=7、e.next=null,执行这两句后结果是newTable[3] =3,e=7,执行完此轮循环后线程A的情况如下

解析:newTable[3] =3,e=7(这里的newTable[3] =3可以理解为指针,其实就是newTable[3] 是个数组,那它的值就是指向3这个节点,e=7的e可以理解为指针,从原本的指向3到指向7)

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

//此时e=7,内存中7.next=3、3.next=null,newTable[3] =3,e=7
//JMM中规定所有的变量都存储在主内存中每条线程都有自己的工作内存,
//线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。
//线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。
//同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。
Entry<K,V> next = e.next;------->next=7.next=3; 
//下面三句就是头插法的实现
 e.next = newTable[i];----------》e.next=3;//注意这里的e还是7,这句就是7的指针指向3
 newTable[i] = e; --------------》newTable[3]=e=7; 
 e = next;----------------------》e=next=3;

注意:JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。

所以上面线程B执行后它的工作内存存储自己的执行结果7.next=3和3.next=null,然后主内存同步过去,最后线程A去主内存复制过来到自己的工作内存中

执行完此轮循环后结果如下

注意:当e=3时3.next=null这个是线程B执行后主内存中的结果

解析:这里唯一要注意的地方就是上面在执行 e.next = newTable[i];的时候结果是e.next=3;而这其中的e是7不是3!这句是把7和3连接起来,从7指向3。这里我一开始就把e.next=3;作为下一次循环的数据依据,然后就觉得应该是下面的图示情况,其实应该是当e=3的时候e.next=null作为数据依据

这种是错误的!

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

//此时e=3,内存中next=3,newTable[3]=7,e=3,e.next=null
//任何一个线程的执行情况应该是放在内存中的,所以并发的时候才会出问题
//例如这个线程A去内存中拿的数据是线程B执行后的数据,已经不是线程A之前存的数据了
Entry<K,V> next = e.next;------->next=null; 
//下面三句就是头插法的实现
 e.next = newTable[i];----------》e.next=7;
 newTable[i] = e; --------------》newTable[3]=3; 
 e = next;----------------------》e=3;

执行完此轮循环后结果如下

当执行完上面的循环后发现next=null后,将不会进行下一轮循环。到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。

  • JDK1.8 中HashMap线程不安全主要体现在数据覆盖

如果有两个线程A、B都在进行put操作(对HashMap进行put操作实际上是调用了putVal()方法),并且这两个线程hash函数计算出的插入下标是相同的,当线程A执行完putVal()方法中的一句用于判断有没有发生hash碰撞的代码后(下面源代码的15行),由于时间片耗尽导致被挂起(注意这个时候线程A的判断结果是没有发生hash碰撞,保存了这个判断结果,但是还没有执行下一句插入元素代码,这个时候被挂起了),而线程B得到时间片后也是调用putVal()方法插入元素,由于两个线程hash函数计算出的下标是一样的,并且前面线程A因为时间片到了还没来得及插入元素就被挂起了,所以这个时候线程B判断结果也是没有hash碰撞,直接在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
 }


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;
	Node<K,V> p; 
	int n, i;
	//如果当前map中无数据,执行resize方法。并且返回n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
	 //判断有没有发生hash碰撞,没有就直接插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
	//否则的话,说明这上面有元素
        else {
            Node<K,V> e; K k;
	    //如果这个元素的key与要插入的一样,那么就替换一下,也完事。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
	    //1.如果当前节点是TreeNode类型的数据,执行putTreeVal方法
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
		//还是遍历这条链子上的数据,跟jdk7没什么区别
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
			//2.完成了操作后多做了一件事情,判断,并且可能执行treeifyBin方法
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null) //true || --
                    e.value = value;
		   //3.
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
	//判断阈值,决定是否扩容
        if (++size > threshold)
            resize();
	    //4.
        afterNodeInsertion(evict);
        return null;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2065284.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【亲测有效】JS Uncaught TypeError: [function] is not a constructor

【亲测有效】JS Uncaught TypeError: [function] is not a constructor 在JavaScript编程中&#xff0c;Uncaught TypeError: [function] is not a constructor 是一个相对常见的错误&#xff0c;通常发生在尝试使用某个值作为构造函数&#xff0c;但实际上它不是构造函数的情况…

鸿蒙卡片服务开发

首先先创建一个项目 在该项目下创建一个卡片服务 在module.json5文件下配置 {"module": {..."extensionAbilities": [{"name": "EntryFormAbility","srcEntry": "./ets/entryformability/EntryFormAbility.ets",…

Linux文件操作:文件描述符fd

文章目录 前言&#xff1a;回顾一下文件提炼一下关于文件的理解&#xff1a; 理解文件&#xff1a;通过系统调用操作文件&#xff1a;理解标志位传参&#xff1a;打开文件 open写入信息 write 理解文件描述符&#xff1a;对于open的返回值&#xff1a;文件描述fd的本质是什么呢…

设计模式之Decorator装饰者、Facade外观、Adapter适配器(Java)

装饰者模式 设计模式的基本原则&#xff0c;对内关闭修改。 Decorator Pattern&#xff0c;装饰者模式&#xff0c;也叫包装器模式(Wrapper Pattern)&#xff1a;将一个对象包装起来&#xff0c;增加新的行为和责任。一定是从外部传入&#xff0c;并且可以没有顺序&#xff0…

Qml 实现仿前端的 Notification (悬浮出现页面上的通知消息)

【写在前面】 经常接触前端的朋友应该经常见到下面的控件&#xff1a; 在前端中一般称它为 Notification 或 Message&#xff0c;但本质是一种东西&#xff0c;即&#xff1a;悬浮弹出式的消息提醒框。 这种组件一般具有以下特点&#xff1a; 1、全局/局部显示&#xff1a;它不…

基于单片机的信号发生器设计

本设计采用了STM32F103C8T6单片机作为控制核心&#xff0c;通过控制DDS模块产生不同频率且高稳定和低失真的信号&#xff0c;再通过放大电路对信号的幅值进行放大。此外通过按键可以使用户对频率进行调节以及对输出波形进行切换&#xff0c;由于AD9833输出的幅值是固定的&#…

启动docker镜像

1、运行容器 2、当前运行的进程 3、当前位置和启动时间 4、cat/etc/redhat-release查看版本 5.镜像是模版&#xff0c;容器是实例 6.容器中没有命令运 7.容器总是能轻易获取 8.配置yum 9.安装http 10.修改index⽂件 11.httpd -k start 12.访问 13.退出就没有服务了 14…

细谈LCM驱动电压VGHVGL电路原理

前言&#xff1a; ***在液晶显示屏驱动电路中&#xff0c;VGH电压负责对TFT栅极电容进行充电开启&#xff0c;并使电容电压保持一个场周期&#xff0c;VGL电压负责TFT栅极的关闭。 如果VGH和VGL电压出现不稳或者幅度变化&#xff0c;都会引起图像显示故障&#xff0c;例如花屏…

委托发布 | 进迭时空联合移动云能力中心实现业界首个RISC-V IO虚拟化方案

仟江水商业电讯&#xff08;8月22日 北京 委托发布&#xff09;虚拟化是云计算技术基石&#xff0c;是RISC-V走进云计算等高性能计算场景的必然要求。RISC-V国际基金会2021年制定了Hypervisor 1.0规范&#xff0c;2023年制定了AIA 1.0规范和IOMMU 1.0规范&#xff0c;这3个规范…

CentOS 7使用RPM安装MySQL5.7

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1&#xff1a;下载MySQL5.7的rpm安装包2&#xff1a;卸载已安装的MySQL&#xff08;没安装过则跳过&#xff09;3&#xff1a;MySQL安装环境准备4&#xff1a;安…

网络竞赛可视化:打造线上赛事

通过图扑网络竞赛可视化&#xff0c;可以实时跟踪和分析参赛者的表现&#xff0c;直观展示比赛进程和结果。这不仅提高了观赛体验&#xff0c;还帮助组织者更有效地管理和优化赛事。

STM32——SPI通信协议以及软件读写

1、SPI协议 SPI相对于I2C传输速度更快&#xff1b;设计简单&#xff0c;通信协议使用硬件线比较多&#xff0c;有些资源浪费 以下设备需要进行共地&#xff0c;如果从机没有独立的供电源&#xff0c;主机需要给供电 SS线低电平有效&#xff0c;主机只能选择一个从机 推挽输出…

Spring + Boot + Cloud + JDK8 + Elasticsearch 单节点 模式下实现全文检索高亮-分页显示 快速入门案例

1. 安装elasticsearchik分词器插件 sudo wget https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-8.13.4.zip sudo mkdir -p ./es_plugins/analysis-ik sudo mkdir ./es_data sudo unzip elasticsearch-analysis-ik-8.13.4.zip -d ./es_plugins/a…

SpringCloudAlibaba Seata分布式事务

分布式事务 事务是数据库的概念&#xff0c;数据库事务&#xff08;ACID&#xff1a;原子性、一致性、隔离性和持久性&#xff09;&#xff1b; 分布式事务的产生&#xff0c;是由于数据库的拆分和分布式架构(微服务)带来的&#xff0c;在常规情况下&#xff0c;我们在一个进…

自己动手写CPU_step4_逻辑运算|移位指令

序 上一篇中我们解决了流水线的数据相关问题&#xff0c;本篇将添加多条逻辑运算和移位运算指令&#xff0c;这些指令的格式严格按照MIPS的指令格式设计。 MIPS指令格式 由于本人也是处于学习的一个过程&#xff0c;如有不对之处&#xff0c;还请大牛指正。 就逻辑运算和移位运…

【软件逆向】第11课,软件逆向安全工程师之windows API函数,每天5分钟学习逆向吧!

资料获取 关注作者&#xff0c;备注课程编号&#xff0c;获取本课配套课件和工具程序。 干货开始-windows API函数。 微软官方提供的应用程序接口&#xff0c;是一些预先定义的函数&#xff0c;目的是提供应用程序与开发人员基于某软件或硬件提供的能力。 地址&#xff1a;h…

java基础 之 了解final

文章目录 定义使用及规则修饰类修饰方法修饰变量修饰成员变量修饰局部变量final与static共同修饰变量final修饰的变量和普通变量的区别 本篇文章代码就不附上了&#xff0c;建议大家实际敲一敲&#xff0c;更能加快理解 定义 final表示”最后的&#xff0c;最终的“含义&#…

精益思维赋能机器人行业的三大维度

在日新月异的科技浪潮中&#xff0c;机器人行业正以前所未有的速度蓬勃发展&#xff0c;成为推动产业升级与转型的关键力量。然而&#xff0c;如何在激烈的市场竞争中脱颖而出&#xff0c;实现高效、灵活与可持续的发展&#xff1f;精益思维&#xff0c;这一源自制造业的管理哲…

【el-switch更改高、宽、颜色样式】深入浅出element ui的switch同页面存在多个更改样式互不影响

1.技术&#xff1a; “vue”: “^2.6.14”, “element-ui”: “^2.15.6”, 2.需求&#xff1a; 同一个页面存在多个switch组件时&#xff0c; 需要更改各自的高度、宽度、选择颜色、非选中颜色等样式&#xff0c; 并且样式隔离互不影响&#xff01; 3.效果图&#xff1a; 4.重要…

C++动态规划(背包问题)

目录 一&#xff1a;动态规划是什么 二.动态规划的运用 &#xff08;1&#xff09;.用动态规划解决重复子问题 &#xff08;2&#xff09;.动态规划使用的条件与流程 Ⅰ.动态规划的使用条件&#xff1a; Ⅱ.动态规划的使用流程 &#xff08;3&#xff09;.背包问题 三.…