【昕宝爸爸小模块】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<>();
        // 手动同步访问HashMap
        synchronized (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 threshold
		else if (oldThr > 0) {
			newCap = oldThr;
		}
		// zero initial threshold signifies using defaults
		else {
			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 order
					else {
						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;
								else
									hiTail.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不到数据

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

c#图片作为鼠标光标

图片转换为鼠标光标代码如下&#xff1a; private void Form1_Load(object sender, EventArgs e) {//button1.Cursor System.Windows.Forms.Cursors.Hand;Bitmap bmp new Bitmap("780.jpg");Cursor cursor new Cursor(bmp.GetHicon());button1.Cursor cursor;} …

黑马程序员JavaWeb开发|案例:tlias智能学习辅助系统(4)员工管理|修改员工、配置文件

指路&#xff08;1&#xff09;&#xff08;2&#xff09;&#xff08;3&#xff09;&#x1f447; 黑马程序员JavaWeb开发|案例&#xff1a;tlias智能学习辅助系统&#xff08;1&#xff09;准备工作、部门管理_tlias智能学习辅助系统的需求分析-CSDN博客https://blog.csdn.n…

可狱可囚的爬虫系列课程 11:Requests中的SSL

一、SSL 证书 SSL 证书是数字证书的一种&#xff0c;类似于驾驶证、护照、营业执照等的电子副本。SSL 证书也称为 SSL 服务器证书&#xff0c;因为它是配置在服务器上。 SSL 证书是由受信任的数字证书颁发机构 CA 在验证服务器身份后颁发的&#xff0c;其具有服务器身份验证和…

Bom 和 Dom 区别 ----- 真是DOM 和 虚拟Dom区别

DOM和BOM的区别 我们都指代&#xff0c;javascript由三个部分组成&#xff1a; ECMAScript&#xff1a;描述了JS的语法和基本对象 BOM(浏览器对象)&#xff1a;与浏览器交互的方法和对象 DOM(文档对象模型)&#xff1a;处理网页内容的方法和接 ps&#xff1a;根据宿主&#x…

桌面微信多开小工具正式发布

本博客站点已全量迁移至 DevDengChao 的博客 https://blog.dengchao.fun , 后续的新内容将优先在自建博客站进行发布, 欢迎大家访问. 社牛: 我需要同时登录多个微信, 一个微信已经满足不了我了! Android: 让我看看系统里有没有自带的多开功能, 不行的话去应用市场下一个多开软…

DLT:dlt-daemon示例解析2

DLT&#xff1a;dlt-daemon示例解析 回顾一下上期第一个示例打印DLT日志的流程。 这次来分析第二个示例。 目录dlt-daemon/examples/example2/下有以下文件 CMakeLists.txt dlt_id.h example2.c example2.xml 其中example2.xml编译用不到&#xff0c;里面描述了一些程序的…

代码随想录算法训练营第二天|977 有序数组的平方、209长度最小的子数组、59 螺旋矩阵||

977 有序数组的平方 题目链接&#xff1a;有序数组的平方 思路 暴力解法 很容易想到的就是按照题目的说明&#xff0c;先给非递减数组中的每个元素做平方&#xff0c;然后使用一个排序函数对齐进行排序即可。 class Solution { public:vector<int> sortedSquares(ve…

第3章:python的判断语句

学一门语言&#xff0c;无外乎多敲&#xff0c;多用&#xff0c;记得回顾昨天写过的代码呀 布尔类型和比较运算符 布尔类型的定义 使用比较运算符进行比较运算得到布尔类型的结果 比较运算符 """ 演示布尔类型的定义 以及比较运算符的应用 ​ """…

基于OpenMV与STM32的数据通信项目(代码开源)

前言&#xff1a;本文为手把手教学 OpenMV 与 STM32 的数据通信项目教程&#xff0c;本教程使用 STM32F103C8T6 与 OpenMV 进行操作。 OpenMV 是非常强大的计算机视觉实现工具&#xff0c;自身提供了非常多的视觉项目案例&#xff0c;编程与使用门槛极低。为了进一步增强作品的…

内存淘金术:Redis 内存满了怎么办?

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 内存淘金术&#xff1a;Redis 内存满了怎么办&#xff1f; 前言LRU&#xff08;Least Recently Used&#xff09;算法LFU&#xff08;Least Frequently Used&#xff09;算法定期淘汰策略内存淘汰事件…

字体图标 iconFont

字体图标使用场景︰主要用于显示网页中通用、常用的一些小图标 精灵图是有诸多优点的&#xff0c;但是缺点很明显。 图片文件还是比较大的。图片本身放大和缩小会失真。一旦图片制作完毕想要更换非常复杂。 此时&#xff0c;有一种技术的出现很好的解决了以上问题&#xff0c…

day-06 构造有效字符串的最少插入数

思路 动态规划&#xff1a; Word[i]单独组成abc 如果Word[i]>word[i-1] 则word[i]和word[i-1]一起构成abc 解题方法 关系式&#xff1a;dp[i]dp[i-1]2或dp[i]dp[i-1]-1 时间复杂度&#xff1a; O(n) 空间复杂度&#xff1a; O(1) Code class Solution {/*动态规划&…

Uncaught ReferenceError: videojs is not defined

项目场景&#xff1a; 项目背景&#xff1a; 开发 vue 项目时&#xff0c;调试时浏览器前端控制台 出现红色 报错信息&#xff1a; Uncaught ReferenceError: videojs is not defined 问题描述 遇到的问题&#xff1a; 开发 vue 项目时&#xff0c; 浏览器控制台出现如下所…

Python-基础语法

标识符 第一个字符必须是字母表中字母或下划线 _ 。标识符的其他的部分由字母、数字和下划线组成。标识符对大小写敏感。在 Python 3 中&#xff0c;可以用中文作为变量名&#xff0c;非 ASCII 标识符也是允许的了。 python保留字 保留字即关键字&#xff0c;我们不能把它们用…

【Docker】Docker安装入门教程及基本使用

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是Java方文山&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的专栏《Docker实战》。&#x1f3af;&#x1f3af; &…

Google上架:2024年一月政策限制之 AI 生成的内容

为确保 Google Play 用户能够获得安全、值得信赖的使用体验&#xff0c;Google会定期更新开发者计划政策。今天就来讲解一下关于一月新政策《AI 生成的内容》。 目录 公布日期&#xff1a;2023-10-25内容公告相关博客截止时间2024-1-31 公布日期&#xff1a;2023-10-25 内容公…

常用的几种推荐算法介绍

个性化推荐&#xff08;推荐系统&#xff09;经历了多年的发展&#xff0c;已经成为互联网产品的标配&#xff0c;也是 AI 成功落地的分支之一&#xff0c;在电商(淘宝/京东)、资讯(今日头条/微博)、音乐(网易云音乐/QQ音乐)、短视频(抖音/快手)等热门应用中&#xff0c;推荐系…

字符串处理(将字符串中符合十六进制数据格式的数字和字符按照其对应的十进制数值进行累加) C语言xdoj704

题目描述&#xff1a; 输入由数字和字符构成的字符串&#xff08;不包含空格&#xff09;&#xff0c;将字符串中符合十六进制数据格式的数字和字符按照其对应的十进制数值进行累加&#xff0c;并输出累加结果&#xff0c;如果字符串中不含有任何满足十六进制格式的字符&#x…

禁用code server docker容器中的工作区信任提示

VSCode 添加受限模式&#xff0c;主要是防止自动运行代码的&#xff0c;比如在vscode配置的task和launch参数是可以运行自定义代码的。如果用VScode打开未知的工程文件就有可能直接运行恶意代码。 但是当我们的实验基础模板文件可控的情况下&#xff0c;要想禁用code server do…

【libpcap】获取报文pcap的ns级别的时间戳

1.安装libpcap 首先&#xff0c;下载最新的 libpcap 源代码。你可以从 tcpdump.org 获取最新版本 1 解压下载的libpcap tar -zxvf libpcap-version.tar.gz 2 进入解压目录进行安装 cd libpcap-version ./configure make sudo make install2 解析报文时间戳 #include <pca…