首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深度解析 ConcurrentHashMap:从源码到实战

深度解析 ConcurrentHashMap:从源码到实战

作者头像
灬沙师弟
发布2025-11-12 13:28:22
发布2025-11-12 13:28:22
3660
举报
文章被收录于专栏:Java面试教程Java面试教程

一、前言

线程安全

并发度

备注

HashMap

×

——

最快,但多线程下 CPU 100% 死循环曾坑哭无数码农

Hashtable

全表一把锁,基本等同于串行

Collections.synchronizedMap

同上,只是包装器

ConcurrentHashMap

极高

分段/桶级锁 + CAS,读写可并行,扩容亦并发

结论:并发场景下,CHM 是唯一“能扛流量又能打”的哈希表实现。


二、数据结构演进史

  1. JDK 1.5 分段锁(Segment) 16 个可重入锁,理论并发度 16。
  2. JDK 1.8 桶级锁 + CAS 抛弃 Segment,直接 synchronized 锁单个首节点,冲突时才加锁;扩容时多线程协同迁移,CPU 利用率拉满。

三、JDK 1.8 核心成员变量速览

代码语言:javascript
复制
// 位标识
staticfinalint MOVED     = -1; // ForwardingNode
staticfinalint TREEBIN   = -2; // 红黑树根
staticfinalint RESERVED  = -3; // computeIfAbsent 占位

transientvolatile Node<K,V>[] table;      // 主哈希桶
privatetransientvolatile Node<K,V>[] nextTable; // 扩容目标数组
privatetransientvolatilelong baseCount; // 无冲突时直接累加
privatetransientvolatileint sizeCtl;    // 控制标识:负数=正在扩容/-1=初始化/n=扩容阈值

四、put 流程(含并发控制)

  1. 空表首次初始化 通过 CASsizeCtl 从 0 改为 -1,单线程建表,其余线程 Thread.yield() 自旋等待。
  2. 计算哈希 spread(key.hashCode()) 再散列,减少哈希碰撞。
  3. 定位桶 (n - 1) & hash,与 HashMap 一致。
  4. 桶为空 CAS 插入新节点,失败则自旋。
  5. 桶非空 首节点 hash == MOVED → 帮忙扩容 (helpTransfer)。 否则 synchronized 锁定首节点,再按链表/红黑树逻辑插入或更新。
  6. 计数 + 检查扩容 addCount(1L, binCount),若超过 sizeCtl 则触发 transfer

五、get 流程——全程无锁

  1. 计算哈希定位桶。
  2. 首节点匹配直接返回。
  3. 红黑树则按树查找;链表顺序遍历。
  4. 遇到 ForwardingNodenextTable 重新查找。

可见性保障:Node.val 用 volatile 修饰;扩容时旧表只读,新表发布前通过 Unsafevolatile 写保证。


六、扩容(transfer)——多线程协同搬运

  1. 步长 stride 根据 CPU 核数计算每次迁移的桶数(最小 16),减少竞争。
  2. 全局进度索引 transferIndex 从高位往低位分配任务包,CAS 抢占。
  3. 搬运算法 对单个桶加 synchronized,构建新链表/树,插入到新表,旧桶头插 ForwardingNode 占位。
  4. 帮助扩容 插入时发现 MOVED,当前线程领取stride步任务,加快迁移。

七、线程安全计数器

  • baseCount:无竞争时直接累加。
  • **CounterCell[]**:高并发时分散到数组,类似 LongAdder,最终 sumCount() 汇总。

八、红黑树与链表互转阈值

方向

阈值

说明

链表 → 树

8

且桶总数 ≥ 64,否则优先扩容

树 → 链表

6

删除节点后检查


九、实战踩坑指南

复合操作非原子

代码语言:javascript
复制
if (!map.containsKey(k)) map.put(k, v); // 仍有竞态

正确姿势:putIfAbsent(k, v)computeIfAbsent

遍历一致性 Iterator 弱一致性:创建后其他线程增删改不抛 ConcurrentModificationException,但未必可见。

自定义对象作为 key 必须保证不可变且正确实现 hashCode/equals,否则扩容前后定位桶不一致导致“幽灵丢失”。

批量操作 forEachsearchreduce 支持并行化,可指定 parallelismThreshold,在大数据量场景比串行循环快 2~3 倍。


十、性能压测对比(1.8 vs 1.7 vs Hashtable)

测试场景:32 线程,各执行 1 千万次 put。

实现

耗时 (ms)

吞吐量 (ops/us)

Hashtable

28 000

0.36

CHM 1.7

4 100

2.44

CHM 1.8

2 300

4.35

结论:1.8 桶级锁 + 协同扩容,比 1.7 分段锁再提 45%+。


十一、面试高频追问

  1. 为什么 1.8 放弃 Segment? 粒度太粗,且维护 16 个数组指针占用内存;桶级锁已能满足极高并发。
  2. synchronized 性能差? 1.8 对 synchronized 做了大量偏向锁/轻量锁优化,锁定时间极短(仅链表/树操作),CAS 失败概率低。
  3. CAS 自旋浪费 CPU? 仅在空桶写入失败时自旋,冲突概率极低;扩容帮忙机制反而提升吞吐。
  4. 与 ConcurrentSkipListMap 区别? 后者基于跳表,保证有序但 O(log n);CHM 无序 O(1),并发度更高。

小结

ConcurrentHashMap = 哈希表 + CAS 无锁 + 桶级 synchronized + 协同扩容 + 分散计数器, 是 Doug Lea 为 Java 并发世界打造的“性能怪兽”,也是每个 Javaer 必须通关的“面试必考题”。

如果这篇文章帮到了你,记得点个赞并关注。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-09-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java面试教程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、数据结构演进史
  • 三、JDK 1.8 核心成员变量速览
  • 四、put 流程(含并发控制)
  • 五、get 流程——全程无锁
  • 六、扩容(transfer)——多线程协同搬运
  • 七、线程安全计数器
  • 八、红黑树与链表互转阈值
  • 九、实战踩坑指南
  • 十、性能压测对比(1.8 vs 1.7 vs Hashtable)
  • 十一、面试高频追问
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档