ConcurrentHashMap 为什么在 JDK 1.8 中废弃分段锁?
2026年01月23日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常希望考察你以下几个层面的理解:
- 对
ConcurrentHashMap演进历史的了解:你是否清楚其在不同 JDK 版本中的重大改进。 - 对并发控制技术选型的深度理解:不仅仅是知道 “用了什么”,更要理解 “为什么用这个,而不用那个” 背后的设计权衡。
- 对技术方案优劣的批判性分析能力:能否清晰地分析分段锁(Segment Lock)方案存在的固有缺陷,以及新方案(CAS +
synchronized)如何针对性解决这些问题。 - 将并发理论与实际数据结构结合的能力:如何将锁粒度、内存开销、并发度等理论概念,映射到
ConcurrentHashMap这个具体容器的实现细节上。
核心答案
在 JDK 1.8 中,ConcurrentHashMap 废弃了基于 Segment 的分段锁机制,改为采用 Node 数组 + CAS + synchronized 的实现方式。主要原因是:分段锁的并发度受限于 Segment 的数量,且内存开销较大,而在新的方案下,锁的粒度被细化到了单个桶(Node)级别,理论上并发度与数组长度相同,能实现更高的并行性能,同时数据结构变得更简洁,内存利用更高效。
深度解析
原理/机制:JDK 1.7 分段锁的局限性
-
并发度固定,扩展性差:
- 原理:在 JDK 1.7 中,
ConcurrentHashMap内部由一个Segment数组组成,每个Segment本质上是一个独立的ReentrantLock和一个小型的HashMap。锁的粒度是Segment。 - 问题:并发度(即最多可同时执行的写操作数)在创建时就固定了(默认 16),且无法动态扩容。即使数据均匀分布,在极端高并发场景下,对同一个
Segment的访问仍会成为瓶颈。
- 原理:在 JDK 1.7 中,
-
内存开销与访问开销:
- 原理:每次访问都需要进行两次哈希定位(先定位
Segment,再定位桶),性能有一定损耗。同时,每个Segment都继承了ReentrantLock,本身是一个重量级对象,存在额外的内存开销。 - 问题:在小数据量或并发竞争不激烈时,这种固定开销显得不划算。
- 原理:每次访问都需要进行两次哈希定位(先定位
JDK 1.8 新方案的改进
-
更细粒度的锁:
- 原理:数据结构回归与
HashMap相似的Node数组 + 链表/红黑树。锁的粒度从 “段” 细化到了 “桶的头节点”。 - 优势:理论上,只要线程操作的是不同的桶,就完全不会发生锁竞争。并发度上限等于数组长度,可以动态扩容,远高于固定的
Segment数量。
- 原理:数据结构回归与
-
利用现代 CPU 的 CAS 指令实现高效无锁化:
- 原理:对于桶的头节点插入/替换等操作,优先使用 CAS (Compare-And-Swap) 乐观锁。这是一种 CPU 硬件级别的原子操作,无需挂起线程,性能极高。
- 代码示例(伪代码逻辑):
// 插入新节点时的典型逻辑 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) { break; // CAS 成功,插入完成,全程无锁 } // 如果CAS失败,说明发生了竞争,则进入synchronized锁竞争流程 synchronized (f) { // f是桶的头节点 // ... 在同步块内进行链表或红黑树的插入/更新 }
-
使用内置的
synchronized作为冲突后备锁:- 原理:当 CAS 失败(发生哈希冲突)时,只会对当前冲突的桶的头节点使用
synchronized进行加锁。 - 优势:
- 锁粒度极小:仅锁一个桶。
- JVM 持续优化:
synchronized在 JDK 1.6 后引入了偏向锁、轻量级锁、自旋锁、锁消除、锁粗化等大量优化,其性能在低竞争场景下已非常优秀,不再像早期版本那样 “重量级”。 - 减少依赖:不再需要依赖
ReentrantLock,减少了 API 层面的复杂性。
- 原理:当 CAS 失败(发生哈希冲突)时,只会对当前冲突的桶的头节点使用
对比分析与最佳实践
| 特性维度 | JDK 1.7 (分段锁) | JDK 1.8 (CAS + synchronized) |
|---|---|---|
| 数据结构 | Segment 数组 + HashEntry 数组 | Node 数组 + 链表/红黑树 |
| 锁粒度 | Segment(一组桶) | 单个桶的头节点 |
| 并发度 | 固定,创建时决定 | 可扩展,与数组大小相关 |
| 锁机制 | 基于 ReentrantLock | CAS(无锁) + synchronized(后备锁) |
| 内存开销 | 较大(每个Segment都是对象+锁) | 较小(数据结构更紧凑) |
| Hash冲突处理 | 链表 | 链表 -> 红黑树(优化长链表查询) |
最佳实践与常见误区:
- 最佳实践:JDK 1.8 后的
ConcurrentHashMap实现是通用的高性能选择。在开发中,应优先使用最新稳定版 JDK 提供的实现,无需再关心分段锁的概念。 - 常见误区:认为
synchronized一定比ReentrantLock性能差。在ConcurrentHashMap这种 锁竞争时间极短、范围极小 的场景下,经过深度优化的synchronized因其 JVM 原生支持、开销低 的特性,反而是更优选择。ReentrantLock的优势在于 可中断、可超时、公平锁、条件变量 等高级功能,而这些在ConcurrentHashMap的桶锁场景中并非必需。
总结
JDK 1.8 废弃分段锁,是为了追求极致的并发性能和更低的内存开销,通过 “无锁 CAS 尝试 + 极细粒度 synchronized 后备” 的组合策略,将锁竞争的概率和范围降到了最低,这是对硬件特性和 JVM 锁优化成果的完美运用。