当前位置: 首页 > news >正文

ConcurrentHashMap 为什么比 HashMap 安全?

点击上方“java大数据修炼之道”, 选择“设为星标”

技术干货发文后 👉 第一时间奉上

引言

上一篇我们深入分析了 HashMap 的源码,今天来聊聊它的线程安全版本 ConcurrentHashMap。很多人知道"HashMap 线程不安全,多线程用 ConcurrentHashMap",但为什么不安全?ConcurrentHashMap 又是怎么解决的?今天我们从原理层面彻底搞清楚。

一、HashMap 为什么线程不安全?

HashMap 的线程不安全问题主要体现在以下几个场景:

1. 扩容时死循环(JDK 7)

JDK 7 的 HashMap 在扩容时使用头插法转移链表节点。多线程同时触发扩容时,两个线程可能同时操作同一条链表,导致链表形成环形结构。一旦出现环形链表,后续的 get 操作就会陷入死循环,CPU 飙升到 100%。这是 JDK 7 最经典的 HashMap 并发 bug。

JDK 8 改用尾插法解决了死循环问题,但仍然存在数据丢失的风险。

2. 数据丢失

两个线程同时执行 put 操作,如果两个 key 的 hash 值相同,都判断该位置为空,然后都往同一个位置写入,后写入的会覆盖先写入的,导致数据丢失。

3. size 不准确

HashMap 的 size 字段没有任何同步保护,多线程并发 put 时,size 的自增操作不是原子的,最终 size 的值会比实际元素数量少。

二、ConcurrentHashMap 的演进

ConcurrentHashMap 在 JDK 7 和 JDK 8 中的实现方式有很大差异,理解这个演进过程非常重要。

JDK 7:分段锁(Segment)

JDK 7 的 ConcurrentHashMap 采用分段锁设计。整个 Map 被分成若干个 Segment,每个 Segment 继承自 ReentrantLock,相当于一个小的 HashMap。

默认有 16 个 Segment,也就是说最多支持 16 个线程并发写入(每个线程操作不同的 Segment)。读操作不加锁,写操作只锁住对应的 Segment,而不是整个 Map。

这比直接用 synchronized 锁住整个 HashMap(即 Hashtable 的做法)性能好很多,但 Segment 数量固定,并发度有上限。

JDK 8:CAS + synchronized

JDK 8 彻底重写了 ConcurrentHashMap,抛弃了分段锁,改用 CAS + synchronized 的组合方案,并发粒度细化到每个数组槽位(桶)。

核心思路:

  • 数组初始化:用 CAS 保证只有一个线程完成初始化

  • 插入新节点(桶为空):用 CAS 直接写入,不加锁

  • 插入节点(桶不为空):用 synchronized 锁住该桶的头节点

  • 扩容:多线程协作扩容,每个线程负责一段区间

这样锁的粒度从 Segment(默认 16 个)细化到了每个桶(数组长度,默认 16,最大可达 2 的 30 次方),并发性能大幅提升。

三、JDK 8 put 操作详解

理解 put 操作是理解 ConcurrentHashMap 线程安全的关键。整个流程如下:

  1. 计算 key 的 hash 值

  2. 如果数组还没初始化,调用 initTable() 用 CAS 初始化

  3. 根据 hash 定位到数组下标 i,如果该位置为 null,用 CAS 直接插入新节点,成功则结束

  4. 如果该位置正在扩容(节点的 hash 值为 MOVED),当前线程帮助扩容

  5. 否则用 synchronized 锁住该位置的头节点,然后遍历链表或红黑树插入

  6. 插入后检查链表长度,超过 8 则转为红黑树

  7. 用 addCount() 更新元素数量(内部用 LongAdder 思想,分散计数减少竞争)

四、get 操作为什么不加锁?

ConcurrentHashMap 的 get 操作全程不加锁,这是怎么保证线程安全的?

关键在于 Node 节点的 val 和 next 字段都用 volatile 修饰。volatile 保证了可见性,一个线程修改了节点的值,其他线程能立即看到最新值,不会读到脏数据。

数组本身也用 volatile 修饰(通过 Unsafe 的 getObjectVolatile 读取),保证扩容后新数组对所有线程可见。

五、size() 方法的实现

JDK 8 的 ConcurrentHashMap 没有用一个简单的 int 来记录元素数量,而是借鉴了 LongAdder 的思想:

  • 有一个 baseCount 字段,低并发时直接用 CAS 更新

  • 高并发时,更新失败的线程会把增量写入 CounterCell 数组的某个槽位

  • size() 是把 baseCount 和所有 CounterCell 的值加起来

这样避免了多线程竞争同一个计数器,大幅减少了 CAS 失败的次数。

六、ConcurrentHashMap vs Hashtable vs Collections.synchronizedMap

Hashtable:所有方法都加 synchronized,锁的是整个对象,并发度最低,基本已被淘汰。

Collections.synchronizedMap:包装 HashMap,所有操作加同一把锁,和 Hashtable 类似,并发度低。

ConcurrentHashMap:锁粒度细化到桶级别,读不加锁,写只锁单个桶,并发性能最好,是多线程场景的首选。

七、使用注意事项

ConcurrentHashMap 虽然线程安全,但有几个坑要注意:

  • 复合操作不是原子的:先 get 再 put 这种操作不是原子的,多线程下仍然可能出问题。应该用 putIfAbsent、computeIfAbsent 等原子方法

  • 不允许 null 键和 null 值:HashMap 允许 null,但 ConcurrentHashMap 不允许,插入 null 会抛 NullPointerException

  • size() 不是强一致的:size() 返回的是一个估算值,在并发修改时可能不准确

总结

HashMap 线程不安全的根本原因是没有任何同步机制,多线程并发操作会导致死循环、数据丢失、size 不准等问题。ConcurrentHashMap 通过 CAS + synchronized 的组合,将锁粒度细化到单个桶,读操作完全不加锁,写操作只锁单个桶,在保证线程安全的同时实现了高并发性能。

记住一句话:单线程用 HashMap,多线程用 ConcurrentHashMap,别用 Hashtable。

end
===往期精彩文章复习回顾===
1.SpringBoot 插件化开发模式,真香啊! 2.一行代码,实现请假审批流程(Java版) 3.血泪教训,8 个线程池最佳实践和坑 4.SpringBoot骚操作:一个注解秒杀所有类型的文件下载! 5.Controller层代码这么写,同事们都模仿起来了

最近整理一份资料《程序员学习手册》,覆盖了 Java技术、面试题精选、操作系统基础知识、计算机基础知识、Linux教程、计算机网络等等。

获取方式:点“在看,关注公众号Java大数据修炼之道并回复PDF领取,更多内容陆续奉上。

长按识别下方二维码关注后回复关键字:PDF领取

你想学的java知识这里都有,长按下方图片识别关注我们吧~

如喜欢本文请点击右上角,把文章分享到朋友圈 因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享 点分享点收藏点在看
http://www.jsqmd.com/news/573955/

相关文章:

  • 公司SEO推广有哪些常见的误区需要避免
  • Python AOT冷启动从2100ms压至83ms:揭秘字节跳动内部Pymemmap预加载+LLVM ThinLTO增量链接实战(仅限TOP20企业白名单开放)
  • 2026跟随式点胶机源头厂家哪家好?在线式/喷射式点胶机设备厂家深度盘点及推荐:7强 - 栗子测评
  • OpenClaw环境隔离方案:安全运行Kimi-VL-A3B-Thinking高风险任务
  • 2026年Java程序员冲大厂有何经验套路?
  • YOLOv8实战:手把手教你启用VarifocalLoss提升小目标检测精度(附完整代码)
  • Pixel Couplet Gen应用场景:微信小程序‘灵蛇贺岁’互动模块开发全解析
  • SAP 物料组数据显示不全的排查与解决
  • 北京礼品回收服务商综合测评与2026年选购指南 - 2026年企业推荐榜
  • 为什么这些开源项目都选择了Tauri+Rust?从隐私安全到性能优化的深度解析
  • 无GPU方案:OpenClaw低配电脑调用远程Qwen3-14B镜像指南
  • Oracle19c EM Express配置与访问全攻略:从零到可视化管理的实践指南
  • LoRa网关实战:5分钟搞定MQTT通信(附Java代码示例)
  • 2026年靠谱的电力设备回收公司选择指南 - 品牌宣传支持者
  • 电力‘黑话’解析:手把手教你用格西调试精灵测试IEC60870-5-103协议
  • 3个技巧掌握QtScrcpy:免费跨平台安卓投屏终极指南
  • OpenClaw安全实践:Qwen3.5-9B本地化处理敏感图片数据
  • Kandinsky-5.0-I2V-Lite-5s实际作品展示:黄昏女孩转头推进镜头高清视频集
  • SEO_从零开始,手把手教你制定完整的SEO方案
  • 2026年邛崃地下室防水服务商深度测评:五大实力派谁更胜一筹? - 2026年企业推荐榜
  • 手把手教你用Python脚本自动化计算Flask Debug PIN(附避坑指南)
  • Pixhawk+OpenMV实战:如何用Apriltag实现无人机自动降落(附避坑指南)
  • 别再乱加注意力了!深入聊聊SE模块的适用场景与三大使用误区
  • 从单卡4090到8卡A100:五款开源数字人模型部署配置清单与避坑指南
  • A股闪崩策略全解析:从数据接口选股到实时交易执行的完整流程
  • OpenClaw自动化测试:Qwen3.5-9B验证UI截图与设计稿一致性
  • UDE Memtool实战:从零到一完成AURIX MCU程序烧录
  • 告别PX4,试试APM!用ArduPilot+Gazebo搭建你的第一个无人机仿真环境(附QGC地面站连接)
  • OpenClaw长期运行维护:千问3.5-35B-A3B-FP8系统资源监控与优化
  • Keil5为STM32F103添加ARM Compiler 5 (AC5) 和解决头文件缺失(device.h/cmsis.h)全记录