文章目录

  • 一、HashMap 的“不安全”:问题的根源
    • 1. 数据结构回顾 (JDK 1.8)
    • 2. 并发下的致命缺陷:`put` 操作
  • 二、ConcurrentHashMap 的安全之道 (JDK 1.8+)
    • 1. 核心数据结构
    • 2. 安全的 `put` 操作:分场景精细化加锁
    • 3. 安全的 `size()` 计算:并发计数
  • 三、表格总结

在 Java 面试和日常开发中, HashMapConcurrentHashMap (以下简称 CHM) 是我们绕不开的两个核心类。我们都知道它们的关键区别是“线程安全”: HashMap 是非线程安全的,而 CHM 是线程安全的。

那么,这种“安全”是如何实现的?CHM 到底比 HashMap 高明在哪里?为什么 JDK 1.8 的 CHM 性能远超早期版本?

本文将带你深入 JDK 1.8+ 的源码,彻底搞懂 CHM 并发安全的底层奥秘。

一、HashMap 的“不安全”:问题的根源

在分析 CHM 的精妙设计之前,我们必须先理解 HashMap 在并发环境下为什么会“翻车”。

1. 数据结构回顾 (JDK 1.8)

HashMap 的底层是 “数组 + 链表 / 红黑树”

// HashMap.java
transient Node<K,V>[] table;

table 是一个 Node 数组。当多个元素的 hash 值冲突时,它们会以链表的形式存放在同一个数组索引(桶)下;当链表长度超过 TREEIFY_THRESHOLD (默认为8) 且数组长度大于 64 时,链表会转化为红黑树以提高查询效率。

2. 并发下的致命缺陷:put 操作

HashMap 的所有操作都没有任何同步措施。我们以 putVal 方法为例,看看在并发下会发生什么。

// HashMap.java -> putVal() 核心逻辑简化
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 1. 检查 table 是否为空,如果为空则初始化 (resize)if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 2. 计算索引 i,检查该位置是否为空if ((p = tab[i = (n - 1) & hash]) == null)// 3. 如果为空,直接创建一个新节点放在这里tab[i] = newNode(hash, key, value, null);else {// ... 省略桶内已有元素时的处理逻辑}// ...return null;
}

经典的“数据覆盖”竞争条件 (Race Condition):

假设两个线程 T1 和 T2 同时向一个空桶 tab[i]put 数据。

  1. T1 执行到第 2 步,p = tab[i],发现 pnull
  2. 此时发生线程切换,T2 开始执行。
  3. T2 也执行到第 2 步,p = tab[i],发现 p 同样是 null
  4. T2 继续执行第 3 步,成功将自己的新节点放入 tab[i]
  5. 线程切换回 T1。由于 T1 之前已经判断过 pnull,它不会再检查一次,而是直接执行第 3 步,将自己的新节点放入 tab[i]
  6. 结果:T1 的数据覆盖了 T2 的数据,导致 T2 的写入操作丢失

除了数据覆盖,并发下的 resize() 操作在 JDK 1.7 中甚至会因头插法导致链表形成环形结构,造成 get 操作时死循环。虽然 JDK 1.8 改为尾插法修复了此问题,但数据丢失等并发问题依然存在。

结论HashMap 的不安全,源于其所有操作都是“非原子性”的,且缺乏内存可见性保障。


二、ConcurrentHashMap 的安全之道 (JDK 1.8+)

JDK 1.8 对 CHM 进行了革命性的重构,摒弃了 JDK 1.7 的分段锁(Segment)机制,采用了更为精细的 CAS + synchronized + volatile 方案,实现了更高的并发性能。

1. 核心数据结构

// ConcurrentHashMap.java
transient volatile Node<K,V>[] table;// Node 定义 (部分)
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;       // value 是 volatile 的volatile Node<K,V> next; // next 指针是 volatile 的
}

关键点

  • table 数组被 volatile 修饰:保证其可见性,当一个线程对 table 进行扩容或修改后,其他线程能立刻看到。
  • Nodevalnext 指针被 volatile 修饰:保证了节点值或链表结构的修改对其他线程的可见性。这是无锁读(get 操作)的关键基础。

2. 安全的 put 操作:分场景精细化加锁

CHM 的 put 方法是其并发设计的精髓所在。它通过 CASsynchronized 的配合,将锁的粒度降到了最低。

我们来分析其核心方法 putVal 的源码:

// ConcurrentHashMap.java -> putVal() 核心逻辑简化
final V putVal(K key, V value, boolean onlyIfAbsent) {// ... key/value 不允许为 nullint hash = spread(key.hashCode());int binCount = 0;// (1) 无限循环,直到成功插入或找到已有keyfor (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;// (2) 场景一:table 未初始化,CAS 初始化if (tab == null || (n = tab.length) == 0)tab = initTable(); // CAS操作,只有一个线程能成功// (3) 场景二:目标桶为空,CAS 插入节点 (无锁)else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))break; // CAS 成功,直接跳出循环}// (4) 场景三:检测到正在扩容else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f); // 帮助其他线程一起扩容// (5) 场景四:目标桶有值,锁住头节点else {V oldVal = null;synchronized (f) { // f 是桶的头节点if (tabAt(tab, i) == f) { // 再次确认头节点没变// 在同步块内,遍历链表或红黑树if (fh >= 0) {// ... 链表遍历逻辑 ...} else if (f instanceof TreeBin) {// ... 红黑树处理逻辑 ...}}}// ...break;}}addCount(1L, binCount); // 更新 sizereturn null;
}

剖析其安全机制:

  • 无锁读(get 操作):得益于 tableNode.valNode.next 都是 volatile 的,get 操作不需要加任何锁。volatile 保证了读线程总能看到其他写线程对数据的最新修改,从而实现了高效的无锁读取。

  • put 操作的安全性分析

    1. 初始化安全 (场景二)initTable() 方法内部使用 CAS 操作来设置 sizeCtl 状态位,保证了在多线程同时初始化时,只有一个线程能成功执行,其他线程则会 yield 等待。
    2. 空桶插入安全 (场景三):当目标桶为空时,CHM 并未直接加锁,而是乐观地尝试使用 CAS (casTabAt) 操作将新节点放入。
      • casTabAt 是一个原子操作,它会比较 tab[i] 的内存值是否为 null,如果是,就将其更新为新节点。
      • 如果 CAS 成功,说明没有竞争,操作完成。
      • 如果失败,说明在它操作的瞬间,有另一个线程捷足先登了。此时 for 循环会继续,重新读取 tab[i] 的新值,进入下一个场景(通常是场景五)。
    3. 非空桶插入安全 (场景五):这是最关键的部分。当桶中已有节点时,不能再用 CAS 了(因为需要遍历链表)。此时,CHM 采用 synchronized 关键字,但它锁住的不是整个 table,而是这个桶的头节点 f
      • 锁粒度极小:只锁住当前要操作的桶,其他桶的操作完全不受影响,并发度极高。
      • 为什么不锁 StringIntegersynchronized 锁的是对象。如果锁 key,当 key 是常量池中的字符串时,可能会锁住一个全局共享的对象,造成意想不到的死锁。锁住桶的头节点 Node 对象是最安全和高效的选择。
    4. 扩容安全 (场景四):当一个线程在 put 时发现桶的头节点 hash 值为 MOVED,表示 table 正在扩容。此时它不会等待,而是调用 helpTransfer 加入到扩容大军中,帮助移动数据,充分利用了 CPU 资源,实现并发扩容。

3. 安全的 size() 计算:并发计数

HashMapsize 只是一个简单的成员变量 int size。多线程下 ++size 是非原子操作,会导致计数不准。

ConcurrentHashMapsize() 实现非常巧妙,它避免了全局锁,以分散计数的方式实现。

  • baseCount:一个基础计数值,在没有并发竞争时,优先通过 CAS 更新这个值。
  • CounterCell[]:一个计数器单元数组。当 CAS 更新 baseCount 失败(说明存在竞争)时,线程会找到 CounterCell 数组中的一个槽位,对这个槽位里的计数值进行更新。
  • size() 计算:最终的大小是 baseCount 加上所有 CounterCell 数组中计数的总和。

这种条带化计数(Striped Counter) 的思想,将对单一 size 变量的竞争压力分散到多个 CounterCell 上,极大地提高了高并发下的计数性能。


三、表格总结

特性HashMap (JDK 1.8+)ConcurrentHashMap (JDK 1.8+)
线程安全
核心数据结构Node[] tablevolatile Node<K,V>[] table
加锁机制无锁CAS + synchronized(锁桶头节点)
put 操作非原子性,并发下有数据覆盖风险1. 空桶:CAS 无锁插入
2. 非空桶:synchronized 锁住桶头节点
get 操作非线程安全无锁,通过 volatile 保证可见性
size() 计算int size 成员变量,非线程安全baseCount + CounterCell[],并发安全高效
扩容机制单线程扩容,并发下危险并发扩容,多线程可协同完成
Key/Value可为 null均不可为 null (避免二义性)
性能单线程下极高高并发下性能优异,单线程下因 volatileCAS 略逊于HashMap

结论:

  • volatile 作为基石,保证了多线程间的内存可见性,是无锁读和后续操作的基础。
  • CAS 作为先锋,处理无竞争或低竞争场景(如初始化、空桶插入),避免了不必要的加锁开销。
  • synchronized 锁住桶头节点作为主力,在必须同步的场景下,将锁的粒度降到最低,实现了极高的并发度。
  • 用并发扩容和并发计数等辅助策略,将并发思想贯彻到每一个细节,榨干硬件性能。

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

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

相关文章

【Java + Vue 实现图片上传后 导出图片及Excel 并压缩为zip压缩包】

系统环境&#xff1a; Java JDK&#xff1a;1.8.0_202 Node.js&#xff1a;v12.2.0 Npm&#xff1a;6.9.0 Java后端实现 Controller /*** xxxx-导出* param response 返回信息体* param files 上传的图片文件* param param1 参数1* param param2 参数2*/PostMapping("/ex…

安科瑞:能源微电网助力工业园区“绿色”发展

朱以真近日&#xff0c;厦门市工业和信息化局印发工业园区绿色智慧微电网建设&#xff0c;拟开展全市工业园区绿色智慧微电网试点通知&#xff0c;那么对于如何实现绿色园区的建设是今天的话题。对工业园区绿色智慧微电网建设需求&#xff0c;其核心价值体现在“源-网-荷-储-充…

VUE2 学习笔记3 v-on、事件修饰符、键盘事件

事件处理v-on用于事件交互。语法&#xff1a;v-on:要绑定的事件“事件触发时执行的函数” &#xff08;函数这里可以写括号&#xff0c;也可以不写&#xff0c;没有影响&#xff09;简写&#xff1a;:事件触发时要执行的函数&#xff0c;在Vue配置参数中&#xff0c;通过method…

变换域通讯系统CCSK的matlab仿真

CCSK&#xff08;Cyclic Code Shift Keying&#xff09;通信系统的MATLAB仿真。实现完整的CCSK调制、AWGN信道传输和解调过程&#xff0c;并计算了误码率&#xff08;BER&#xff09;。 % CCSK通信系统仿真 clear; clc; close all;% 参数设置 L 31; % m序列…

技术演进中的开发沉思-40 MFC系列:多线程协作

今天说说MFC的线程&#xff0c;当年用它实现中间件消息得心应手之时&#xff0c;可以实现一边实时接收数据&#xff0c;一边更新界面图表图文信息&#xff0c;顺滑得让人想吹声口哨。 MFC 多线程它像给程序装上了分身术&#xff0c;让原本只能 “单任务跑腿” 的代码&#xff0…

高速公路自动化安全监测主要内容

近年来&#xff0c;随着社会经济的快速发展&#xff0c;高速公路的通车里程不断增加&#xff0c;交通流量日益增大。与此同时&#xff0c;高速公路交通事故数量也呈现出一定的增长趋势。这些事故不仅造成了大量的人员伤亡和财产损失&#xff0c;还严重影响了社会的稳定和经济的…

完美解决 Ubuntu 中自定义启动器图标重复的问题(以 MATLAB 为例)

如果你在 Ubuntu 上为 MATLAB、PyCharm、Android Studio 或其他第三方应用创建了自定义启动器&#xff0c;并把它固定到了左侧的 Dock 栏&#xff0c;你很可能遇到过这个令人抓狂的场景&#xff1a; 当你满心欢喜地点击固定的图标启动程序后&#xff0c;Dock 栏上竟然“凭空”冒…

一文读懂深度模型优化器,掌握炼丹工具

深度模型优化器是训练神经网络的核心工具&#xff0c;其目标是高效地找到损失函数的最小值。从基础的随机梯度下降&#xff08;SGD&#xff09;到结合一阶动量修正与两阶段更新的Ranger&#xff0c;优化器的发展始终围绕着加速收敛、提升稳定性、适应参数差异和增强泛化能力四大…

关于虚拟机部署的k8s集群calico组件在开机恢复快照时的问题

在恢复虚拟机开机快照的时候总会出现各种的网络问题&#xff0c;连接不到自己的443端口&#xff0c;集群状态异常等等&#xff0c;绕一圈都发现不了问题&#xff0c;其实根本就没问题。虚拟机环境再现&#xff0c;恢复快照后的k8s环境&#xff0c;nfs莫名宕掉提示此报错&#x…

从感知到决策:虚拟仿真系统与视觉算法融合下的多路RTSP视频接入技术探究

1️⃣ 背景概述 随着国防信息化和智能化进程的不断加快&#xff0c;虚拟仿真系统 与 智能视觉算法平台 正逐步成为现代化装备研发、测试验证与战术训练的重要技术支撑。相比传统的静态建模或离线推演&#xff0c;新一代作战仿真与智能系统更强调 实时性、感知能力与动态交互&a…

Android app如果不适配Android 15会怎么样

深度思考完成 用时49秒 首先&#xff0c;用户的问题是&#xff1a;“Android app如果不适配Android 15会怎么样&#xff1f;”我需要基于搜索结果来回答。 让我回顾搜索结果&#xff1a; ‌1&#xff1a;标题是“Android 15应用适配指南”&#xff0c;内容强调了不适配Androi…

Vue-21-利用Vue3的axios+Python的flask实现前后端交互功能

文章目录 1 任务需求 2 前端 2.1 模板template 2.2 JS代码script 2.3 Calculate.vue(子组件) 2.4 App.vue(根组件) 3 后端 3.1 导入模块 3.2 创建应用实例 3.3 配置CORS 3.4 定义路由 3.5 处理请求 3.6 main.py 4 附录 4.1 CORS 4.1.1 全局启用CORS 4.1.2 限制允许的域名(更安…

动态规划之最长回文子串

题目&#xff1a;最长回文子串 给你一个字符串 s&#xff0c;找到 s 中最长的 回文 子串。 示例 1&#xff1a; 输入&#xff1a;s “babad” 输出&#xff1a;“bab” 解释&#xff1a;“aba” 同样是符合题意的答案。 示例 2&#xff1a; 输入&#xff1a;s “cbbd” 输…

Linux 编程中的错误处理机制详解 —— `errno` 全解析

文章目录Linux 编程中的错误处理机制详解 —— errno 全解析一、什么是 errno&#xff1f;❓为什么需要 errno&#xff1f;✅ 它在哪里定义&#xff1f;二、errno 的设置与读取规则⚠️ errno 不是总是有效&#xff01;❗使用 errno 的正确步骤&#xff1a;三、与 errno 配套使…

力扣-最长递增子序列

简单记录学习~给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。示例…

公司内部网址怎么在外网打开?如何让外网访问内网的网站呢?

很多公司内部本地会部署有中小型的服务器&#xff0c;可以很好的方便用于一些办公业务系统&#xff0c;或测试开发需要。在数字化办公和生活场景中&#xff0c;除了公司内部局域网内访问公司系统外&#xff0c;经常会遇到需要让外网访问内网网站的情况。比如企业员工远程办公时…

有趣的css - 多选立体标签按钮

&#x1f36d; 大家好&#xff0c;我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;今天分享的是一个交互较完整的多选立体标签按钮。 最新文章通过公众号「设计师工作日常」发布。 目录整体效果核心代码html 代码css 部分代码完整代码如下html 页面css 样式页面渲…

C++中byte*和char*的区别

在C中&#xff0c;byte*&#xff08;通常指 std::byte*&#xff09;和 char* 都是指针类型&#xff0c;但它们在语义和用途上有重要区别&#xff1a;1. 类型定义char* char 是C内置的基本类型&#xff0c;表示字符&#xff08;通常是1字节&#xff09;。 char* 常用于&#xff…

【node】npm包本地开发与调试

npm link 进入本地的 babel-plugin-function-try-catch 这个 npm 包的根目录执行&#xff1a; npm link上面的命令可以将当前的这个包安装在全局&#xff08;mac 中的路径是 /usr/local/bin&#xff09;&#xff0c;也就是 npm i -g 安装包的目录。 执行后结果如图&#xff…

突破量子仿真瓶颈:微算法科技MLGO量子算法的算术化与核操作迭代模型

近年来&#xff0c;量子计算机的迅速发展和潜在的强大计算能力吸引了全球科研机构和企业的广泛关注。量子计算机利用量子力学的特性来处理复杂的计算任务&#xff0c;具有在某些方面远超经典计算机的潜力。然而&#xff0c;真正实用的量子计算机尚未大规模普及&#xff0c;因此…