【昕宝爸爸小模块】HashMap用在并发场景存在的问题

在这里插入图片描述

HashMap用在并发场景存在的问题

  • 一、✅典型解析
    • 1.1 ✅JDK 1.8中
    • 1.2 ✅JDK 1.7中
    • 1.3 ✅如何避免这些问题
  • 二、 ✅HashMap并发场景详解
    • 2.1 ✅扩容过程
    • 2.2 ✅ 并发现象
  • 三、✅拓展知识仓
    • 3.1 ✅1.7为什么要将rehash的节点作为新链表的根节点
    • 3.2 ✅1.8是如何解决这个问题的
    • 3.3 ✅除了并发死循环,HashMap在并发环境还有啥问题


这是一个非常典型的问题,但是只会出现在1.7及以前的版本,1.8之后就被修复了。


一、✅典型解析


1.1 ✅JDK 1.8中


虽然JDK 1.8修复了某些多线程对HashMap进行操作的问题,但在并发场景下,HashMap仍然存在一些问题。

如:虽然JDK 1.8修复了多线程同时对HashMap扩容时可能引起的链表死循环问题但在JDK 1.8版本中多线程操作HashMap时仍然可能引起死循环,只是原因与JDK 1.7不同。此外,还存在数据丢失和容量不准确等问题

在并发场景下,HashMap主要存在以下问题:


1. 死循环问题:在JDK 1.8中,引入了红黑树优化数组链表,同时改成了尾插,按理来说是不会有环了,但是还是会出现死循环的问题,在链表转换成红黑数的时候无法跳出等多个地方都会出现这个问题。


2. 数据丢失问题:在并发环境下,如果一个线程在获取头结点和hash桶时被挂起,而这个hash桶在它重新执行前已经被其他线程更改过,那么该线程会持有一个过期的桶和头结点,并且会覆盖之前其他线程的记录,从而造成数据丢失。

3. 容量不准问题:在多线程环境下,HashMap的容量可能不准确。这是因为在进行resize(调整table大小)的过程中,如果多个线程同时进行操作,可能会导致数组链表中的链表形成循环链表,使得get操作时e = e.next操作无限循环,从而无法准确计算出HashMap的容量。

在并发场景下使用HashMap需要注意其存在的问题,并采取相应的措施进行优化和改进。


1.2 ✅JDK 1.7中


在JDK 1.7中,HashMap在并发场景下存在问题。

首先,如果在并发环境中使用HashMap保存数据,有可能会产生死循环的问题,造成CPU的使用率飙升。这是因为HashMap中的扩容问题。当HashMap中保存的值超过阈值时,将会进行一次扩容操作。在并发环境下,如果一个线程发现HashMap容量不够需要扩容,而在这个过程中,另外一个线程也刚好进行扩容操作,就有可能造成死循环的问题。

其次,HashMap在JDK 1.7中并不是线程安全的,因此在多线程环境下使用HashMap需要额外的同步措施来保证并发安全性。否则,可能会导致数据不一致或者出现其他并发问题。

因此,在JDK 1.7中,HashMap在并发场景下也存在一些问题需要注意和解决。


1.3 ✅如何避免这些问题


为了避免在并发场景下使用HashMap时出现的问题,可以下几种方法:

  1. 使用线程安全的HashMap实现:Java提供了线程安全的HashMap实现,如ConcurrentHashMap。ConcurrentHashMap采用了分段锁的机制,可以保证在多线程环境下对HashMap的读写操作都是安全的。

  2. 手动同步:如果必须使用HashMap,可以在访问HashMap时进行手动同步。使用synchronized关键字将访问HashMap的代码块包装起来,保证同一时间只有一个线程可以访问HashMap,从而避免并发问题。

  3. 使用Java并发包中的数据结构:Java提供了一些并发包(java.util.concurrent),其中包含了一些线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等。这些数据结构在内部已经进行了优化,可以保证在多线程环境下的安全性和性能。

避免在并发场景下使用HashMap时出现问题的关键是选择合适的线程安全的实现或手动进行同步操作,以确保数据的一致性和正确性


看下面的这些Demo,解释了如何避免在并发场景下使用HashMap时出现的问题:


1. 使用线程安全的HashMap实现(ConcurrentHashMap


import java.util.concurrent.ConcurrentHashMap;public class ConcurrentHashMapExample {public static void main(String[] args) {ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();// 添加元素到ConcurrentHashMap中concurrentHashMap.put("key1", "value1");// 获取元素String value = concurrentHashMap.get("key1");System.out.println("Value for key1: " + value);}
}

2. 手动同步(使用synchronized关键字


import java.util.HashMap;
import java.util.Map;public class SynchronizedHashMapExample {public static void main(String[] args) {Map<String, String> map = new HashMap<>();// 手动同步访问HashMapsynchronized (map) {// 添加元素到HashMap中map.put("key1", "value1");// 获取元素String value = map.get("key1");System.out.println("Value for key1: " + value);}}
}

3. 使用Java并发包中的数据结构(如 ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;public class AtomicIntegerExample {public static void main(String[] args) {ConcurrentHashMap<String, AtomicInteger> concurrentHashMap = new ConcurrentHashMap<>();// 添加元素到ConcurrentHashMap中,使用AtomicInteger作为值,保证线程安全concurrentHashMap.put("key1", new AtomicInteger(1));// 获取并自增AtomicInteger的值,保持线程安全int newValue = concurrentHashMap.get("key1").incrementAndGet();System.out.println("New value for key1: " + newValue);}
}

二、 ✅HashMap并发场景详解


2.1 ✅扩容过程


HashMap在扩容的时候,会将元素插入链表头部,即头插法。如下图,原来是 A->B->C ,扩容后会变成 C->B->A


看一张图片:


在这里插入图片描述

之所以选择使用头插法,是因为JDK的开发者认为,后插入的数据被使用到的概率更高,更容易成为热点数据,而通过头插法把它们放在队列头部,就可以使查询效率更高


我们再来看一眼源码:


void transfer(Entry[] newTable) {Entry[] src = table;int newCapacity = newTable.length;for (int j = 9; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash, newCapacity);//节点直接作为新链表的根节点e.next = newTableli];newTable[i] = e;e = next;} while (e != null);}}
}

2.2 ✅ 并发现象


但是,正是由于直接把当前节点作为链表根节点的这种操作,导致了在多线程并发扩容的时候,产生了循环引用的问题。


假如说此时有两个线程进行扩容,thread-1 执行到 Entry<K,> next = e.next; 的时候被hang住,如下图所示:


在这里插入图片描述

此时 thread-2 开始执行,当 thread-2 扩容完成后,结果如下:


在这里插入图片描述

此时 thread-1 抢占到执行时间,开始执行e.next = newTable[i]; newTable[i] = e; e = next;后,会变成如下样式:


在这里插入图片描述

接着,进行下一次循环,继续执行 e.next = newTable[i]; newTable[i] = e; e = next; ,如下图所示:


在这里插入图片描述

因为此时 e != null,且 e.next = null,开始执行最后一次循环,结果如下:


在这里插入图片描述

可以看到,a和b已经形成环状,当下次get该桶的数据时候,如果get不到,则会一直在a和b直接循环遍历,导致CPU飙升到100%。


三、✅拓展知识仓


3.1 ✅1.7为什么要将rehash的节点作为新链表的根节点


在重新映射的过程中,如果不将 rehash 的节点作为新链表的根节点,而是使用普通的做法,遍历新链表中的每一个节点,然后将rehash的节点放到新链表的尾部,伪代码如下:


void transfer(Entry[] newTable) {for (int j = ; j < src.length; j++) {Entry<K,V> e = src[j];if (e != null) {src[j] = null;do {Entry<K,V> next = e.next;int i = indexFor(e.hash , newCapacity);//如果新桶中没有数值,则直接放进去if (newTable[i] == null) {newTable[i] = e;continue;}// 如果有,则遍历新桶的链表else {Entry<K,V> newTableEle = newTable[i];while(newTableEle != null) {Entry<K,V> newTableNext = newTableEle .next;//如果和新桶中链表中元素相同,则直接替换if(newTableEle.equals(e)) {newTableEle = e;break;}newTableEle = newTableNext ;}// 如果链表遍历完还没有相同的节点,则直接插入if(newTableEle == null) {newTableEle = e;}}} while (e != null);}}
}

通过上面的代码我们可以看到,这种做法不仅需要遍历老桶中的链表,还需要遍历新桶中的链表,时间复杂度是O(n^2),显然是不太符合预期的,所以需要将rehash的节点作为新桶中链表的根节点,这样就不需要二次遍历,时间复杂度就会降低到O(N)


3.2 ✅1.8是如何解决这个问题的


前面提到,之所以会发生这个死循环问题,是因为在JDK 1.8之前的版本中,HashMap是采用头插法进行扩容的,这个问题其实在JDK 1.8中已经被修复了,改用尾插法!JDK 1.8中的 resize 代码如下:


final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == nul1) ?  : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}else if  ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {newThr = oldThr << 1; // double threshold}// initial capacity was placed in thresholdelse if (oldThr > 0) {newCap = oldThr;}// zero initial threshold signifies using defaultselse {newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this,newTab,j,oldCap);// preserve orderelse {Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next ;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;else loTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;} 
}

3.3 ✅除了并发死循环,HashMap在并发环境还有啥问题


1. 多线程put的时候,size的个数和真正的个数不一样

2. 多线程put的时候,可能会把上一个put的值覆盖掉

3. 和其他不支持并发的集合一样,HashMap也采用了fail-fast操作,当多个线程同时put和get的时候,会抛出并发异常

4. 当既有get操作,又有扩容操作的时候,有可能数据刚好被扩容换了桶,导致get不到数据

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

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

相关文章

【SSO】统一授权中心v1.0.0版本正式上线(多租户)

目录 背景 体验 技术栈 菜单 示例 背景 为了方便权限管理、用户登录授权、应用授权等&#xff0c;特地开发了当前的统一授权中心。 体验 邮箱注册即可登录体验 后台系统&#xff1a;https://sso.behappyto.cn/#/switch 技术栈 vue3tsspringbootmybatismysql 菜单 …

SpringBoot用MultipartFile.transferTo传递相对路径的问题

问题描述&#xff1a; 打算给自己的项目添加一个上传文件保存功能&#xff0c;于是我使用MultipartFile.transferTo()来完成这个功能&#xff0c;由于我的项目要部署到服务器&#xff0c;所以我使用了相对路径把上传的文件保存到当前项目的工作目录下&#xff0c;但是报错了&am…

C++上位软件通过Snap7开源库访问西门子S7-200/LOGO PLC/合信M226ES PLC V存储区的方法

前言 在前面例程中谈到了C 通过Snap7开源库S7通信库跟西门子S7-1200PLC/S7-1500PLC以及合信CTMC M226ES PLC/CPU226 PLC通信的方式方法和应用例程。但是遗憾的是Snap7中根据官方资料显示只能访问PLC的 DB区、MB区、C区、T区 、I区、Q区&#xff0c;并没有提到有关如何访问S7-20…

在学习爬虫前的准备

1. 写一个爬虫程序需要分几步 获取网页内容。 我们会通过代码给一个网站服务器发送请求&#xff0c;它会返回给我们网页上的内容。 在我们平时使用浏览器访问服务器内容是&#xff0c;本质上也是向服务器发送一个请求&#xff0c;然后服务器返回网页上的内容。只不过浏览器还会…

K8s Pod详解

1.Pod结构 每个Pod中都可以包含一个或者多个容器&#xff0c;这些容器可以分为两类&#xff1a; 用户程序所在的容器&#xff0c;数量可多可少 Pause容器&#xff0c;这是每个Pod都会有的一个根容器&#xff0c;它的作用有两个&#xff1a; 可以以它为依据&#xff0c;评估整个…

恒创科技:解决Windows服务器磁盘空间不足的问题

​  服务器硬盘的大小是决定空间是否充足的主要因素。但在日常使用中&#xff0c;服务器和网站备份会消耗大量存储空间&#xff0c;如果维护不当&#xff0c;最终将耗尽您的容量。同样&#xff0c;日志文件、临时文件和数据库可以在硬盘驱动器上或回收站中无休止地建立。当您…

手把手教你升级GPT-4,内附详细步骤

目录 1、先介绍一下 GPT 升级 2、第一种: 免费升级 支付宝购买礼品卡给美区 apple id 充值 3、第二种&#xff1a;5分钟快速升级 方法 平时我会在朋友圈分享一些利用 GPT-4 画的图片&#xff0c;比如下面这个扑克牌风格的"黑红小狗武士"。 用 GPT-4 做绘画仅仅是…

如何使用宝塔面板部署Inis博客并实现无公网ip环境远程访问

文章目录 前言1. Inis博客网站搭建1.1. Inis博客网站下载和安装1.2 Inis博客网站测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总…

大创项目推荐 深度学习机器视觉车道线识别与检测 -自动驾驶

文章目录 1 前言2 先上成果3 车道线4 问题抽象(建立模型)5 帧掩码(Frame Mask)6 车道检测的图像预处理7 图像阈值化8 霍夫线变换9 实现车道检测9.1 帧掩码创建9.2 图像预处理9.2.1 图像阈值化9.2.2 霍夫线变换 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分…

ElasticSearch _update_by_query

根据查询条件进行数据更新 UPDATE job_call SET admin_id 0 WHERE admin_id 283; kibana.png 1.其中红色框的位置为query的内容&#xff0c;对应为SQL语句中的WHERE admin_id 283 2.划红色线的位置为修改内容&#xff0c;对应SQL中的SET admin_id 0。如果是更新多个字段s…

手轮脉冲平滑处理笔记

这是一个求手脉倍率((Hw_Control.mult_ratio)与手脉脉冲计数延迟次数即累计过去n次的平均值(Hw_Control.lag_num)之间关系算法的计算过程笔记文档 1、已知 mult_ratio=1时 lag_num=10; mult_ratio=10时 lag_num=20; .mult_ratio==100时 lag_num=30; 以此类推 2、设lag_num…

开关电源PFC电路原理详解及matlab仿真

PFC全称“Power Factor Correction”&#xff0c;意为“功率因数校正”。PFC电路即能对功率因数进行校正&#xff0c;或者说能提高功率因数的电路。是开关电源中很常见的电路。 在电学中&#xff0c;功率因数PF指有功功率P&#xff08;单位w&#xff09;与视在功率S&#xff08…

每日学习更新(LQR+iLQR)

一直想更新一下根据cost to go来推导LQR&#xff0c;之前的话可能会直接套问题&#xff0c;但是对于理论有些困惑&#xff0c;正好最近在学习ilqr轨迹生成/优化&#xff0c;因此来推一下公式&#xff0c;以下参考B站Dr_CAN&#xff0c;链接如下&#xff1a; 【最优控制】5_线性…

Maven在java中的实现(对java的项目进行打包)

前言: 在前面的文章中我们了解了Maven的作用,并在自己的电脑上安装配置好了Maven,也成功的在IDEA中添加了Maven,但是具体的实现还是有一些些小问题,那么接下来,我将带着大家对Java项目进行一次打包,系统的完成一次,并在途中解决一下会出现的问题. 我以图片中选中的这个包为例,…

【Kafka-3.x-教程】-【四】Kafka-消费者-Consumer

【Kafka-3.x-教程】专栏&#xff1a; 【Kafka-3.x-教程】-【一】Kafka 概述、Kafka 快速入门 【Kafka-3.x-教程】-【二】Kafka-生产者-Producer 【Kafka-3.x-教程】-【三】Kafka-Broker、Kafka-Kraft 【Kafka-3.x-教程】-【四】Kafka-消费者-Consumer 【Kafka-3.x-教程】-【五…

用握力器玩谷歌小恐龙游戏(二)

往期回顾 用握力器玩谷歌小恐龙游戏&#xff08;一&#xff09; GS-GAME-PC 前言 专门买了一个电钻&#xff0c;在握力器上钻出了两个孔&#xff0c;用来放两个引脚的按键&#xff0c;这样比之前用热熔胶的方式更干净&#xff0c;更稳定 加了一个18650的电池&#xff0c;之前…

ChatGPT提示词的高级技巧——解锁AI交互新境界

大家好我是在看&#xff0c;记录普通人学习探索AI之路。 这一讲我来介绍几种提示词的高级使用技巧。 1.拆解问题 对于一个一次性很难完整回答的大问题&#xff0c;我们可以将其拆解成若干子问题&#xff0c;再将这些问题逐步输入。 我们来看下面的例子&#xff1a; 2.迭代改…

RLHF与LLM训练的碰撞:寻找最佳实践之路!

了解更多公众号&#xff1a;芝士AI吃鱼 在讨论大型语言模型&#xff08;LLM&#xff09;时&#xff0c;无论是在研究新闻还是教程中&#xff0c;经常提到一个称为“带有人类反馈的强化学习”&#xff08;RLHF&#xff09;的过程。由于RLHF能够将人类偏好纳入优化过程&#xff0…

What does “grep -i“ do?

grep&#xff08; Global Regular Expression Print &#xff09;根据 给定的正则表达式 搜索文本&#xff0c;并将匹配的行打印出来 grep -i 表示查找的过程中忽略大小写 在已安装的rpm包里&#xff0c;查询是否有tree相关的包 rpm -qa | grep -i "tree"在文件中搜…

使用Python打造一个爱奇艺热播好剧提前搜系统

目录 一、系统功能设计 二、数据获取与处理 三、搜索功能实现 四、用户界面设计 五、系统部署与维护 六、总结 随着互联网的普及和人们对于娱乐需求的增加&#xff0c;视频网站成为了人们观看电视剧、电影等视频内容的主要渠道。爱奇艺作为国内知名的视频网站之一&#x…