ConcurrentHashMap原理详解(太细了)
一、什么是ConcurrentHashMap
ConcurrentHashMap
和HashMap
一样,是一个存放键值对的容器。使用hash
算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。
同时,ConcurrentHashMap
是线程安全的HashMap
。专门用于多线程环境。
二、ConcurrentHashMap和HashMap以及Hashtable的区别
2.1 HashMap
HashMap
是线程不安全的,因为HashMap
中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用HashMap
是会抛出异常的。
2.2 HashTable
HashTable
是线程安全的,但是HashTable
只是单纯的在put()
方法上加上synchronized
。保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。
2.3 ConcurrentHashMap
ConcurrentHashMap
是线程安全的,ConcurrentHashMap
并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。
由此可见,HashTable
可真是一无是处…
三、ConcurrentHashMap原理
这一节专门介绍ConcurrentHashMap
是如何保证线程安全的。如果想详细了解ConcurrentHashMap
的数据结构,请参考HashMap
。
3.1 volatile修饰的节点数组
请看源码
//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
transient volatile Node<K,V>[] table;
再看看HashMap
是怎么做的
//HashMap没有用volatile修饰节点数组。
transient Node<K,V>[] table;
显然,HashMap
并不是为多线程环境设计的。
3.2 ConcurrentHashMap的put()方法
//put()方法直接调用putVal()方法
public V put(K key, V value) {return putVal(key, value, false);
}
//所以直接看putVal()方法。
final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0)tab = initTable();else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {V oldVal = null;synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}else if (f instanceof TreeBin) {Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;
}
我来给大家讲解一下步骤把。
public V put(K key, V value) {
首先,put()
方法是没有用synchronized
修饰的。
for (Node<K,V>[] tab = table;;)
新插入一个节点时,首先会进入一个死循环,
情商高的就会说,这是一个乐观锁
进入乐观锁后,
if (tab == null || (n = tab.length) == 0)tab = initTable();
如果tab
未被初始化,则先将tab
初始化。此时,这轮循环结束,因为被乐观锁锁住,开始下一轮循环。
第二轮循环,此时tab
已经被初始化了,所以跳过。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; // no lock when adding to empty bin
}
接下来通过key
的hash
值来判断table中是否存在相同的key,如果不存在,执行casTabAt()
方法。
注意,这个操作时不加锁的,看到里面的那行注释了么// no lock when adding to empty bin
。位置为空时不加锁。
这里其实是利用了一个CAS操作。
CAS(Compare-And-Swap):比较并交换
这里就插播一个小知识,CAS就是通过一个原子操作,用预期值去和实际值做对比,如果实际值和预期相同,则做更新操作。
如果预期值和实际不同,我们就认为,其他线程更新了这个值,此时不做更新操作。
而且这整个流程是原子性的,所以只要实际值和预期值相同,就能保证这次更新不会被其他线程影响。
好了,我们继续。
既然这里用了CAS操作去更新值,那么就存在两者情况。
- 实际值和预期值相同
相同时,直接将值插入,因为此时是线程安全的。好了,这时插入操作完成。使用break;
跳出了乐观锁。循环结束。 - 实际值和预期值不同
不同时,不进行操作,因为此时这个值已经被其他线程修改过了,此时这轮操作就结束了,因为还被乐观锁锁住,进入第三轮循环。
第三轮循环中,前面的判断又会重新执行一次,我就跳过不说了,进入后面的判断。
else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);
这里判断的是tab
的状态,MOVED
表示在扩容中,如果在扩容中,帮助其扩容。帮助完了后就会进行第四轮循环。
终于,来到了最后一轮循环。
else {V oldVal = null;synchronized (f) {if (tabAt(tab, i) == f) {if (fh >= 0) {binCount = 1;for (Node<K,V> e = f;; ++binCount) {K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}else if (f instanceof TreeBin) {Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}
}
上面的判断都不满足时,就会进入最后的分支,这条分支表示,key
的hash
值位置不为null
(之前的判断是hash
值为null
时直接做插入操作),表示发生了hash
冲突,此时节点就要通过链表的形式存储这个插入的新值。Node
类是有next
字段的,用来指向链表的下一个位置,新节点就往这插。
synchronized (f) {
看,终于加排它锁了,只有在发生hash冲突的时候才加了排它锁。
if (tabAt(tab, i) == f) {if (fh >= 0) {
重新判断当前节点是不是第二轮判断过的节点,如果不是,表示节点被其他线程改过了,进入下一轮循环,
如果是,再次判断是否在扩容中,如果是,进入下一轮循环,
如果不是,其他线程没改过,继续走,
for (Node<K,V> e = f;; ++binCount) {
for循环,循环遍历这个节点上的链表,
if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;
}
找到一个hash值相同,且key也完全相同的节点,更新这个节点。
如果找不到
if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;
}
往链表最后插入这个新节点。因为在排他锁中,这些操作都可以直接操作。终于到这插入就基本完成了。
总结
做插入操作时,首先进入乐观锁,
然后,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器,
如果已经初始化,则判断该hash
位置的节点是否为空,如果为空,则通过CAS操作进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。
如果没有扩容,则进行最后一步,先加锁,然后找到hash
值相同的那个节点(hash冲突),
循环判断这个节点上的链表,决定做覆盖操作还是插入操作。
循环结束,插入完毕。
3.3 ConcurrentHashMap的get()方法
//ConcurrentHashMap的get()方法是不加锁的,方法内部也没加锁。
public V get(Object key)
看上面这代码,ConcurrentHashMap
的get()
方法是不加锁的,为什么可以不加锁?因为table
有volatile
关键字修饰,保证每次获取值都是最新的。
//Hashtable的get()是加锁的,所以性能差。
public synchronized V get(Object key)
再看看Hashtable
,差距啊。
四、使用场景
嗯,多线程环境下,更新少,查询多时使用的话,性能比较高。
乐观锁嘛,认为更新操作时不会被其他线程影响。所以时候再更新少的情况下性能高。
对你有帮助吗?点个赞吧~
相关文章:

ConcurrentHashMap原理详解(太细了)
一、什么是ConcurrentHashMap ConcurrentHashMap和HashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。 同时,ConcurrentHashMap是线程安全的HashMap。专门用于多线程环境。 二、Concurre…...

EasyExcel根据对应的实体类模板完成多个sheet的写入与读取
1.展示模板一的实体类 import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.write.style.ColumnWidth; import com.alibaba.excel.annotation.write.style.ContentRowHeight; import com.alibaba.excel.annotation.write.style.HeadRowH…...

在企业数字化转型过程中,IT运维发挥着怎样的价值?
IT运维软件在企业数字化转型中发挥着重要的价值。从效率、稳定性、安全性和资源利用率以及数据分析决策支持都有巨大的提升。 提高效率 利用自动化巡检功能,实时或定时进行系统巡检,减少人力巡检的繁琐和低效,避免手动操作的失误,…...

01-工厂模式 ( Factory Pattern )
工厂模式 Factory Pattern 摘要实现范例 工厂模式(Factory Pattern)提供了一种创建对象的最佳方式 工厂模式在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象 工厂模式属于创建型模式 摘要 1. 意图 …...

【LeetCode】每日一题 2024_2_2 石子游戏 VI(排序、贪心)
文章目录 LeetCode?启动!!!题目:石子游戏 VI题目描述代码与解题思路 LeetCode?启动!!! 题目:石子游戏 VI 题目链接:1686. 石子游戏 VI 题目描述…...

一站式在线协作开源办公软件ONLYOFFICE,协作更安全更便捷
1、ONLYOFFICE是什么? ONLYOFFICE是一款功能强大的在线协作办公软件,可以创建编辑Word文档、Excel电子表格,PowerPoint(PPT)演示文稿、Forms表单等多种文件。ONLYOFFICE支持多个平台,无论使用的是 Windows、…...

Java进击框架:Spring-综合(十)
Java进击框架:Spring-综合(十) 前言Rest ClientsWebClientRestTemplateHTTP接口 JMS (Java消息服务)使用Spring JMS发送消息接收消息注释驱动的侦听器端点 JMXEmail任务执行和调度Spring TaskExecutor 抽象Spring TaskScheduler 抽象支持调度…...

2024年第九届信号与图像处理国际会议(ICSIP 2024)
2024第九届信号与图像处理国际会议(ICSIP 2024)将于2024年7月12-14日在中国南京召开。ICSIP每年召开一次,在过去的七年中吸引了1200多名与会者,是展示信号和图像处理领域最新进展的领先国际会议之一。本次将汇集来自亚太国家、北美…...

webassembly003 MINISIT mnist/convert-h5-to-ggml.py
数据结构 # Convert MNIS h5 transformer model to ggml format # # Load the (state_dict) saved model using PyTorch # Iterate over all variables and write them to a binary file. # # For each variable, write the following: # - Number of dimensions (int) # …...

fetch和axios的区别
概念不同 Fetch是一种新的获取资源的接口方式,可以直接使用Axios是一个基于XMLHttpRequest封装的工具包,需要引入才可以使用 传递数据的方式不同 Fetch则是需要放在body属性中,以字符串的方式进行传递Axios是放到data属性里,以对象…...

【unity小技巧】FPS简单的射击换挡瞄准动画控制
文章目录 射击动画控制换弹动画瞄准动画完结 射击动画控制 换弹动画 调用 瞄准动画 问题:瞄准时,但是动画会卡住,不会播放瞄准的待机动画 修改 调用 动画如果太快可以去修改播放速度 播放速度变慢了,可能导致切换待机动画也…...

如何获取时间戳
在JavaScript中,你可以使用Date对象来获取时间戳。以下是一个例子: javascriptvar timestamp new Date().getTime(); console.log(timestamp); 在这个例子中,new Date()创建了一个新的日期对象,.getTime()方法则返回自1970年1月…...

VSCode 设置代理
Open Visual Studio Code, click the settings icon in the lower left corner, and click Settings....

保姆级教程: 零门槛制作AI微信红包封面之入门篇
写在前面 本文旨在低门槛制作微信红包教程,人人均可上手! 操作步骤 AI红包制作平台: https://cover.fdfs.site 第一步: 先登录 alt text 可以使用谷歌,github直接登录,也可以用自己的邮箱注册 第二步: 设置自己的apiKey API-Key可以从平台 ht…...

Redis核心技术与实战【学习笔记】 - 17.Redis 缓存异常:缓存雪崩、击穿、穿透
概述 Redis 的缓存异常问题,除了数据不一致问题外,还会面临其他三个问题,分别是缓存雪崩、缓存击穿、缓存穿透。这三个问题,一旦发生,会导致大量的请求积压到数据库。若并发量很大,就会导致数据库宕机或故…...

Leetcode—2670. 找出不同元素数目差数组【简单】
2024每日刷题(一零七) Leetcode—2670. 找出不同元素数目差数组 哈希表实现代码 class Solution { public:vector<int> distinctDifferenceArray(vector<int>& nums) {unordered_set<int> s;int n nums.size();vector<int&g…...

App ICP备案获取iOS和Android的公钥和证书指纹
依照《工业和信息化部关于开展移动互联网应用程序备案工作的通知》,向iOS和安卓平台提交App时需要先提交ICP备案信息。 iOS平台: 1、下载appuploader工具:Appuploader home -- A tool improve ios develop efficiency such as submit ipa to…...

猿创征文 | 项目整合KafkaStream实现文章热度实时计算
个人简介: > 📦个人主页:赵四司机 > 🏆学习方向:JAVA后端开发 > ⏰往期文章:SpringBoot项目整合微信支付 > 🔔博主推荐网站:牛客网 刷题|面试|找工作神器 > &#…...

状态压缩 笔记
棋盘式的f[i][j]中表示状态的j可以是状态本身也可以是在合法状态state中的下标 用状态本身比较方便,用下标比较省空间 用下标的话可以开id[M]数组记录一下 蒙德里安的梦想 求把 NM的棋盘分割成若干个 12的长方形,有多少种方案。 例如当 N2࿰…...

Java 数据结构篇-实现二叉搜索树的核心方法
🔥博客主页: 【小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录 1.0 二叉搜索树的概述 2.0 二叉搜索树的成员变量及其构造方法 3.0 实现二叉树的核心接口 3.1 实现二叉搜索树 - 获取值 get(int key) 3.2 实现二叉搜索树 - 获取最小…...

go语言(二十一)---- channel的关闭
channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显示的结束range循环之类的,才去关闭channel。关闭channel后,无法向channel再发送数据,(引发pannic错误后,导致接收…...

【PyQt】01-PyQt下载
文章目录 前言静态库 一、PyQt是什么?二、安装1.Windows环境下安装安装PyQt5Designer 2.Liunx环境下安装 总结 前言 拜吾师 PyQt5 快速入门 静态库 补充一点知识: Windows: .lib Linux: .a .so(动态库) 简单描述PyQt就是python调用C的Qt文…...

不一样的味觉体验:精酿啤酒与烤肉的绝妙搭配
在繁华的都市生活中,人们总是在寻找那份与众不同的味觉享受。当夏日的微风轻轻拂过,你是否想过,与三五好友围坐在一起,拿着Fendi Club啤酒与烤肉的绝妙搭配,畅谈生活点滴,感受那份惬意与自在? F…...

linux系统ansible的jiaja2的语法和简单剧本编写
jianja2语法和简单剧本 jinja2语法Jinja default()设定if语句for语句 ansiblejiaja2的使用ansible目录结构:tasks目录下文件内容:nginx模板文件ansible变量文件ansible主playbook文件测试并执行:查看检测执行结果 剧本编写安装apache安装mysq…...

Three.js PBR 物理渲染
详解 Three.js PBR 物理渲染 Three.js 是一个流行的基于 WebGL 的 JavaScript 库,专门用于创建和运行三维动画和游戏。其中很关键的一部分是物理渲染(PBR)。本文将深入探讨 Three.js 的 PBR 渲染,并为初学者提供实用的指导。 什…...

POSIX(包含程序的可移植性) -- 详解
1. 什么是 POSIX 参考链接–知乎 POSIX 标准包含了进程管理、文件管理、网络通信、线程和同步、信号处理等方面的功能。 这些接口定义了函数、数据类型和常量等,为开发者提供了一个可移植的方法来与操作系统进行交互。 2. 谁遵守这个标准 遵守 POSIX 标准的主要是…...

Jmeter学习系列之五:基础线程组(Thread Group)
前言 线程组是一系列线程的集合,每一个线程代表着一个正在使用应用程序的用户。在 jmeter 中,每个线程意味着模拟一个真实用户向服务器发起请求。 在 jmeter 中,线程组组件运行用户设置线程数量、初始化方式等等配置。 例如,如果你设置线程数为 100,那么 jmeter 将创建…...

Android 双卡适配 subId 相关方法
业务场景 双卡设备进行网络等业务时,需要正确操作对应的卡。 执行卡业务和主要是使用subId和 PhoneId/SlotId进行区分隔离。 代码举例 初始化subId //初始化subId private int mSubId SubscriptionManager.INVALID_SUBSCRIPTION_ID;//1、通过intent传值&#x…...

使用Logstash将MySQL中的数据同步至Elasticsearch
目录 1 使用docker安装ELK 1.1 安装Elasticsearch 1.2 安装Kibana 1.3 安装Logstash 2 数据同步 2.1 准备MySQL表和数据 2.2 运行Logstash 2.3 测试 3 Logstash报错(踩坑)记录 3.1 记录一 3.1.1 报错信息 3.1.2 报错原因 3.1.3 解决方案 3.2 记录二 3.2.1 报错信…...

米贸搜|Facebook公共主页反馈分数(ACE) 更新
前段时间Meta改进了公共主页反馈分数的仪表板,发现有部分广告主似乎没有接受到这条动态,今天为大家整理出更新内容,方便各位广告主了解学习! Meta重新设计了公共主页反馈分数仪表板,以便广告主能更轻松地了解总体反馈…...