CurrentHashMap的整体系统介绍及Java内存模型(JVM)介绍

news2025/5/10 23:17:51

当我们提到ConurrentHashMap时,先想到的就是HashMap不是线程安全的:

在多个线程共同操作HashMap时,会出现一个数据不一致的问题。

ConcurrentHashMap是HashMap的线程安全版本。

它通过在相应的方法上加锁,来保证多线程情况下的数据一致性。

hashmap导致数据不一致的原因?

数据不一致问题的表象有两种情况:

1.写-读冲突:一个线程修改后,另一个线程读到的不是最新的数据。

2.写-写冲突:两个线程同时修改数据,发生数据覆盖的情况。

原因是Java内存模型(JVM)的一些相关规定。

Java内存模型(JVM)

Java内存模型将内存分为两种,主内存工作内存。

并且规定,所有的变量都存储在主内存中(不包括局部变量与方法参数)。

主内存中的变量是所有线程共享的。

每个线程都有自己的工作内存,存储的是当前线程所使用到的变量值。主内存变量中的一个副本数据。

线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。

不同线程间无法直接访问对方工作内存中的变量。

线程间变量值的传递需要通过主内存实现。

这样规定的原因:

是为了屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

关于各种硬件间的内存访问差异

CPU,内存,IO设备都在不断迭代,不断朝着更快的方向努力,但三者的速度是有差异的。

CPU最快,内存其次,IO设备(硬盘)最慢。

为了合理利用CPU的高性能,平衡三者间的速度差异,计算机体系结构,操作系统,编译系统都做了贡献,主要体现为:

  • CPU增加了缓存,以平衡与内存的速度差异,

这样CPU运算时所需要的变量,优先会从缓存中读取。

缓存没有时,会从主内存中加载并缓存。如下图所示:

image-20250509163155756

事物都是有两面性的,缓存提高了CPU的运算速度,也带来了相应的问题:

当多个线程在不同的CPU上运行并访问同一个变量时,由于缓存的存在,可能读取不到做最新的值,也就是可见性问题。

可见性指的是一个线程对共享变量的修改,另一个线程能够立刻看到,被称为可见性

  • 操作系统增加了进程线程,以时分复用CPU,进而均衡CPU与IO设备的速度差异

操作系统通过任务的一个切换来减少CPU的等待时间,从而提高效率。

任务切换的时间,可能是发生在任何一条CPU指令执行完之后。

但是我们平时使用的编程语言,如C,Java,Python等都是高级语言,高级语言转换成CPU指令时,一条指令可能对应多条CPU指令。 相当于1=n,这是违背我们直觉的地方。

所以问题来了,著名的count+=1问题就是这个原因。也就是原子性问题。

我们把一个或多个操作在CPU执行的过程中不被中断的特性为原子性。(这里的操作是指我们高级语言中相应的一些操作)

  • 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用。

指令重排序可以提高了缓存的利用率,同样也带来了有序性问题

也就是单例模式问题

重排序提高缓存利用率的例子:

在平时写代码时,经常会在方法内部的开始位置,把这个方法用到的变量全部声明了一遍。缓存的容量是有限的,声明的变量多的时候 前面的变量可能就会在缓存中失效 。

接下来再写业务时,用到了最先声明的变量 然后发现在缓存中已经失效了,需要重新的去主内存进行加载。

所以指令重排序可以看成编译器对我们写的代码进行的一个优化。就类似于让变量都能用上,不至于等到失效在使用。

所以要想实现在各种平台都能达到一直的内存访问效果,就需要解决硬件和操作系统之间产生的问题:

1.CPU增加缓存最后导致的可见性问题

2.操作系统增加了线程,进程之后出现的原子性问题

3.指令重排序导致的有序性问题

Java内存模型如何解决三个问题?

原子性问题解决方案

  • JVM定义了8种操作来完成主内存与工作内存之间的数据交互,虚拟机在实现时需要保证每一种操作都是原子的,不可再分的。

Java中基本数据类型的访问、读写都是具备原子性的(long和Double除外),更大的原子性保证:Java提供了synchronized关键字(synchronized的字节码指令monitorenter和monitorexit来隐式的使用了lock和unlock操作),在synchronized块之间的操作也具备原子性。

八种操作: lock,unlock,read,load,assign,use,store,write

CAS(乐观锁),比较并替换,(Compare And Swap),CAS是一条CPU的原子指令(即cmpxchg指令),Java中的Unsafe类提供了相应的CAS方法,如(compareAndSwapXXX)底层实现即为CPU指令cmpxchg,从而保证操作的原子性。

可见性问题与有序性问题解决方案

  • JVM定义了Happens-Before原则来解决内存的不可见性与重排序的问题。

Happens-Before规则约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后要遵守Happens-Before规则。

Happens-Before规则:

对于两个操作A和B,这两个操作可以在不同的线程中执行,如果A Happens-Before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作时可见的。

8种Happens-Before规则

程序次序规则、锁定规则、volatile变量规则、线程启动规则、线程终止规则、线程中断规则、对象终结原则、传递性原则。

volatile变量规则(重点):对一个volatile变量的写操作先行发生于后面的这个变量的读操作。

hashmap导致数据不一致的解决方案

常规思路是加锁,但是锁的存在会大大影响性能,所以提升性能的关键就是减少锁的粒度,以及找出哪些操作可以无锁化

对于写操作:涉及到对数据的改动,需要加锁,这只能尽量减少锁的粒度。

对于读操作:确保数据改动不会出错之后,读操作就相对好办;主要考虑的能不能读到另外一个线程对数据的一个改动(一致性)(等待写操作的完成)

这时就有三种情况:

  1. 强一致性 : 读写都加锁,类似于串行化,这样可以保证读到最新的数据,但性能过低

  2. 顺序一致性 : 变量使用volatile关键字修饰

  3. 弱一致性 : 读不加锁

对应方案:

  1. 强一致性 使用synchronized 修饰方法或者代码块,来保证代码块或方法的一致性,可见性(串行,即有序性),性能较低

  2. 顺序一致性 : 使用volatile关键字修饰变量,volatile 可以保证一个共享变量的可见性以及禁止指令的重排序

  3. 弱一致性: 使用CAS,CAS操作可以保证一个共享变量的原子操作。

我们可以去读一下ConcurrentHashMap的源码,

可以发现代码中一会使用CAS,一会使用synchronized,让人摸不清,为什么呢?

这是因为在高级语言中一条语句往往需要多条CPU指令完成

而Java中基本数据类型的访问、读写都具备原子性(long和Double除外),其他大部分不是原子性操作,

就比如在new一个对象时,就不是一个原子性操作,它需要三步才能完成,分配内存,初始化对象,将对象赋值给变量。

所以在创建数组的时候,除了使用synchronized外,CAS是不能保证原子性的,CAS只是CPU的一条指令,他不能保证多个指令的原子性,但是我们可以参考AQS,使用CAS锁一个基本类型的变量,其他线程进行自旋。

其次,synchronized锁需要一个对象,当数组的元素为null时,是无法使用synchronized锁的,所以此时使用的就是CAS操作来保证赋值的原子性。

以及底层的数组table已经被volatile修饰,但是数组元素的修改却不能保证可见性

明明volatile保证共享变量的可见性,为什么数组元素的修改却不能保证可见性呢?

原因:

volatile保证共享变量的可见性,但是如果该变量是一个对象的引用,那么volatile此时指的就是对象引用的可见性。

而在Java中,数组也是一个对象,当使用volatile来修饰数组arr时,代表的是arr的引用具有可见性,即arr的引用地址修改了之后,其他线程是可见的,但是无法保证数组内的元素具有可见性。

HashTable与ConcurrentHashMap

Hashtable

前置知识:在JDK1.0时,加锁只有synchronized一种方法,synchronized是重量级锁(需要去CPU申请锁)

底层结构:数组+链表 链表使用头插法 定位数组下标使用取余操作

线程安全: 使用synchronized来保证线程安全,在所有的方法上都加了synchronized关键字,即使用一把全局锁来同步不同线程间的并发访问(锁住整个table结构),性能较低。

相关操作: put,get,remove,size方法体上都添加synchronized关键字,扩容逻辑在put方法内发生,也是线程安全的

优点:实现简单

缺点:一个线程在插入数据时,其他线程不能读写,并发效率低下

ConcurrentHashMap(JDK1.5)

在JDK1.5时引入,此时Java内存模型已经成熟完善,在此基础上开发了java.util.concurrent包,ConcurrentHashMap随着JUC包一起引入JDK,同时引入了AQS,实现了ReentrantLock

底层结构:数组+链表 链表使用头插法 定位下标使用&运算

线程安全:使用分段锁的思想,其内部是一个Segment数组,Segment继承了ReentrantLock(可重复锁),即Segment自身就是一个锁。

Segment内部有一个HashEntry数组(Segment有点类似HashTable),每个HashEntry是一个链表结构的元素,一把锁只锁住容器中的一部分数据,多线程访问容器中里不同数据段的数据,就不会存在锁竞争,提高并发访问率

相关操作:调用put方法时,当前的segment会将自己锁住,此时其他线程无法操作这个segment,但不会影响到其他segment的操作。

调用get方法时,使用unsafe.getObjectVolatile方法获取节点;底层使用C++的volatile来实现Java中的volatile效果(保证共享变量的可见性(一个线程对共享变量的修改,另一个线程能够立刻看到))

调用remove方法时,当前的segment会将自己锁住。

put,get,remove操作都是在单个Segment上进行的,size操作是在多个segment进行的

size方法采用了一种比较巧妙的方式,来尽量避免对所有的Segment都加锁。

每个Segment都有一个modCount 变量,代表的是对Segment中元素的数量造成影响的操作次数。这个值只增不减。

size 操作就是遍历了两次Segment,每次记录Segment 的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回。如果不相同,则把这个过程再重复做一次,如果再不同,则就需要将所有的Segment都锁住,然后一个一个遍历。

扩容操作,发生在put方法内部,跟put方法使用的是同一个锁.

扩容不会增加Segment的数量,只会增加Segment中链表数组的容量大小

这样的好处是扩容过程不需要对整个ConcurrentHashMaprehash,只需要对Segment里面的元素做一个rehash即可。这样就不会去影响其他的segment里面的元素。

优点:每次只锁住一部分数据,访问不同数据段的数据,不会存在锁竞争。提高了并发访问率;

扩容只针segment内部的HashEntry数组进行扩容,不影响其他segment内部的HashEntry数组。

缺点:定位一个元素,需要经过两次hash操作。 当某个segment很大时,类似Hashtable,性能会下降。

比较浪费内存空间(因为每个segment内部的HashEntry数组是不连续的)

拓展:

在JDK6中,针对synchronized做了大量的优化,引入了轻量级锁偏向锁。性能与ReentrantLock已相差无几,甚至synchronized的自动释放锁会更好用。

Java官方表示,在多线程环境下不建议使用HashMap。

随着互联网的快速发展,业务场景随之更加复杂,很多人在使用多线程的情况下使用HashMap的时候,结果导致cpu100%的情况。

主要原因:HashMap的链表使用的是头插法,在多线程的情况下触发扩容,链表可能会形成一个死循环。

在JDK8中也做了相应的优化,将头插法改为尾插法,引入了红黑树,来优化链表过长导致的查询速度变慢。

连带着ConcurrentHashMap也做了相应的修复,使得ConcurrentHashMap与HashMap的结构更加统一。

ConcurrentHashMap(JDK8之后)

image-20250509190726003

由类图可知,ConcurrentHashMap中有四种类型的节点,四种类型的节点的用途不同。

  • Node节点是ConcurrentHashMap中存储数据的最基本结构,也是其他类型节点的父类,他可以用来构建链表。hash值>=0

  • TreeNode节点主要用来构造红黑树以及存储数据hash值>=0

  • TreeBin节点是红黑树的代理节点,不存储数据,他的Hash值是一个固定值-2

  • ForWardingNode节点,表示的是底层数组table正在扩容,当前节点的数据已经迁移完毕,不存储数据,hash值也是固定值-1

注意事项:TreeBin为什么是红黑树的代理节点?

因为在向红黑树添加数据或删除数据时可能会触发红黑树的自平衡,根节点可能会被子节点替代,如果此时有线程来红黑树读取数据,可能会出现读取不到数据的情况。

而红黑树的查找是从根节点开始遍历的,当根节点变成子节点时,作为根节点的左子树或者右子树可能是不被遍历的。

ConcurrentHashMap的get方法是没有使用锁的,不可能通过加锁来保证强一致性,而红黑树的并发操作需要加上一层锁来保证在红黑树自平衡时的读操作没有问题。这就是TreeBin的工作。

TreeBin重要属性:

  • root:指向的是红黑树的根节点

  • first:指向的是双向链表,也就是所有的TreeNode节点构成的一个双向链表

  • lockState:用于实现基于CAS的读写锁。

总结:对红黑树添加或删除数据的整体操作:

首先在最外层加上synchronized同步锁,然后再红黑树自平衡时加上lockState的写锁。

当由线程来读红黑树的时候,会先判断此时是否有线程持有写锁或者是否有线程在等待获取写锁,如果有的话,读线程直接读取双向链表,否则会加上lockState的读锁。然后读取红黑树的数据,从而来保证读操作不被阻塞以及它的正确性。

双向链表的作用:

  • 读操作会来读取链表上的数据。

  • 在扩容时,会遍历双向链表,根据hash值判断是放在新数组的高位还是低位。

底层结构:数组+链表+红黑树 链表使用尾插法 定位下标使用 & 运算

线程安全:消了分段锁的设计,1取而代之的是通过 cas 操作和 synchronized 关键字来保证并发更新的安全。

Synchronized只是用于锁住链表或者红黑树的第一个节点,只要没有Hash冲突,就不存在并发问题,效率也就大大的提升。

相关操作:

put方法,使用cas + synchronized 来保证线程安全.

get方法,没有使用加锁,使用的是Unsafe.getObjectVolatile方法获取数据。保证数据的可见性。

remove方法、使用synchronized 来保证线程安全。

size方法(难点):主要是LongAdder的思想进行的累加计算。

扩容操作(难点):扩容操作发生在数据添加成功之后,并且支持多个线程。

优点:锁粒度更精细,性能更强

缺点:实现更加复杂。

希望对大家有所帮助!

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

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

相关文章

spring ai alibaba 使用 SystemPromptTemplate 很方便的集成 系统提示词

系统提示词可以是.st 文件了,便于修改和维护 1提示词内容: 你是一个有用的AI助手。 你是一个帮助人们查找信息的人工智能助手。 您的名字是{name} 你应该用你的名字和{voice}的风格回复用户的请求。 每一次回答的时候都要增加一个65字以内的标题形如:【…

网络的搭建

1、rpm rpm -ivh 2、yum仓库(rpm包):网络源 ----》网站 本地源 ----》/dev/sr0 光盘映像文件 3、源码安装 源码安装(编译) 1、获取源码 2、检测环境生成Ma…

C++学习之类和对象_1

1. 面向过程与面向对象 C语言是面向过程的,注重过程,通过调用函数解决问题。 比如做番茄炒蛋:买番茄和鸡蛋->洗番茄和打鸡蛋->先炒蛋->把蛋放碟子上->炒番茄->再把蛋倒回锅里->加调料->出锅 而C是面向对象的&#xff…

YOLOv12云端GPU谷歌免费版训练模型

1.效果 2.打开 https://colab.research.google.com/?utm_sourcescs-index 3.上传代码 4.解压 !unzip /content/yolov12-main.zip -d /content/yolov12-main 5.进入yolov12-main目录 %cd /content/yolov12-main/yolov12-main 6.安装依赖库 !pip install -r requirements.…

OpenCV进阶操作:图像直方图、直方图均衡化

文章目录 一、图像直方图二、图像直方图的作用三、使用matplotlib方法绘制直方图2.使用opencv的方法绘制直方图(划分16个小的子亮度区间)3、绘制彩色图像的直方图 四、直方图均衡化1、绘制原图的直方图2、绘制经过直方图均衡化后的图片的直方图3、自适应…

基环树(模板) 2876. 有向图访问计数

对于基环树,我们可以通过拓扑排序去掉所有的树枝,只剩下环,题目中可能会有多个基环树 思路:我们先利用拓扑排序将树枝去掉,然后求出每个基环树,之后反向dfs求得所有树枝的长度即可 class Solution { publi…

【物联网】基于树莓派的物联网开发【1】——初识树莓派

使用背景 物联网开发从0到1研究,以树莓派为基础 场景介绍 系统学习Linux、Python、WEB全栈、各种传感器和硬件 接下来程序猫将带领大家进军物联网世界,从0开始入门研究树莓派。 认识树莓派 正面图示: 1:树莓派简介 树莓派…

学习Python的第一天之网络爬虫

30岁程序员学习Python的第一天:网络爬虫 Requests库 1、requests库安装 windows系统通过管理员打开cmd,运行pip install requests!测试案例: 2、Requests库的两个重要对象 Response对象Resoponse对象包含服务器返回的所有信息&#xff…

linux下的Redis的编译安装与配置

配合做开发经常会用到redis,整理下编译安装配置过程,仅供参考! --------------------------------------Redis的安装与配置-------------------------------------- 下载 wget https://download.redis.io/releases/redis-6.2.6.tar.gz tar…

无人机相关技术与故障排除笔记

无人机相关技术与故障排除笔记 本文档整理了关于无人机电调、电机、通信协议、传感器以及硬件故障排除相关的笔记和解释。 1. 电调 (ESC) PWM 输出初始化设置 初始化电调(电子调速器)的 PWM 输出功能时,设置 频率 400Hz、分辨率 10000、初…

SpringSecurity(自定义异常处理)

文末有本篇文章的项目源码可供下载学习。 在实际的项目开发过程中,我们对于认证失败或者授权失败需要像接口一样返回相同结构的json数据,这样可以让前端对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。 在SpringSecu…

Java——反射

目录 5 反射 5 反射 类信息:方法、变量、构造器、继承和实现的类或接口。反射:反射是 Java 中一项强大的特性,它赋予了程序在运行时动态获取类的信息,并能够调用类的方法、访问类的字段以及操作构造函数等的能力。通过反射&#…

本地玩AI绘画 | StableDiffusion安装到绘画

环境须知 Cuda必须安装 不需要安装Python,因为该项目会自动安装Python3.10的虚拟环境 1.下载StableDiffusionWebUI压缩包并解压 下载方式一: 从Github下载https://github.com/AUTOMATIC1111/stable-diffusion-webui 的压缩包,解压后名为…

project从入门到精通(四)

目录 日程表的设置和妙用 为日程表视图添加任务 用日程表视图的好处 ​编辑 查找任务的前置任务和后续任务 方法1:采用复合视图的方式 方法3:关系图法 方法4:通过任务路径的方式检查所选任务的前置任务 前置任务和驱动前置任务的区…

git项目迁移,包括所有的提交记录和分支 gitlab迁移到gitblit

之前git都是全新项目上传,没有迁移过,因为迁移的话要考虑已有项目上的分支都要迁移过去,提交记录能迁移就好;分支如果按照全新项目上传的方式需要新git手动创建好老git已有分支,在手动一个一个克隆老项目分支代码依次提…

港大今年开源了哪些SLAM算法?

过去的5个月,香港大学 MaRS 实验室陆续开源了四套面向无人机的在线 SLAM 框架:**FAST-LIVO2 、Point-LIO(grid-map 分支) 、Voxel-SLAM 、Swarm-LIO2 **。这四套框架覆盖了单机三传感器融合、高带宽高速机动、长时间多级地图优化以…

Godot4.3类星露谷游戏开发之【昼夜循环】

千里之行,始于足下 文章目录 零、 笔记一、创造时间二、产生颜色三、搭建测试环境四、测试五、免费开源资产包 零、 笔记 为了让游戏可以拥有白天和黑夜,我们需要像上帝一样,在游戏中创造时间的规则,并在不同的时间点产生不同的颜…

修复笔记:获取 torch._dynamo 的详细日志信息

一、问题描述 在运行项目时,遇到与 torch._dynamo 相关的报错,并且希望获取更详细的日志信息以便于进一步诊断问题。 二、相关环境变量设置 通过设置环境变量,可以获得更详细的日志信息: set TORCH_LOGSdynamo set TORCHDYNAM…

Windows平台下的Qt发布版程序打包成exe可执行文件(带图标)|Qt|C++

首先先找一个可执行文件的图标 可以去阿里的矢量图库里找 iconfont-阿里巴巴矢量图标库 找到想要的图标下载下来 此时的图标是png格式的,我们要转到icon格式的文件 要使用到一个工具Drop Icons_2.1.1.rar - 蓝奏云 生成icon文件后把icon文件放到你项目的根目录下…

CSS--图片链接垂直居中展示的方法

原文网址&#xff1a;CSS--图片链接垂直居中展示的方法-CSDN博客 简介 本文介绍CSS图片链接垂直居中展示的方法。 图片链接 问题复现 源码 <html xml:lang"cn" lang"cn"><head><meta http-equiv"Content-Type" content&quo…