ConcurrentHashMap 为什么在 JDK 8 中废弃分段锁?
ConcurrentHashMap 为什么在 JDK 8 中废弃分段锁?
zhangchangsheng面试考察点
- 架构演进理解:面试官不仅仅是想知道 “废弃了” 这个事实,更是想知道你是否理解分段锁的设计局限性,以及 JDK 1.8 新方案解决了哪些问题。
- 性能权衡思维:考察你是否理解锁粒度、内存开销、并发度之间的权衡,以及为什么 “更简单的 synchronized” 反而比 “复杂的 ReentrantLock” 更好。
- JVM 优化认知:是否了解 JDK 1.6 后 synchronized 的锁升级机制,以及它如何改变了并发编程的最佳实践。
核心答案
JDK 1.8 废弃分段锁的主要原因:分段锁并发度固定、内存开销大、实现复杂,而 CAS + synchronized 方案并发度更高、内存更省、实现更简洁: **** **** **** **** ****
| 对比维度 | JDK 1.7 分段锁 | JDK 1.8 CAS + synchronized |
|---|---|---|
| 并发度 | 最多 16(Segment 数量) | 等于桶数量(默认 16,可扩容) |
| 锁粒度 | 一个 Segment 锁多个桶 | 一个锁只锁一个桶 |
| 内存开销 | 每个 Segment 独立对象 | 无额外锁对象 |
| 扩容灵活性 | Segment 数量固定 | 桶数量动态增长 |
| 实现复杂度 | 复杂(双重哈希、Segment 管理) | 简洁(直接操作桶) |
一句话总结:分段锁是 “中间粒度” 的妥协方案,CAS + synchronized 实现了 “最细粒度” 的完美方案,并发度更高、内存更省。
深度解析
一、分段锁的设计局限性
| 局限 | 说明 |
|---|---|
| 1. 并发度固定,无法动态扩展 | Segment 数量 = concurrencyLevel(默认 16),即使 HashMap 扩容到 65536 个桶,仍然只有 16 个 Segment(每个 Segment 管约 4096 个桶)。桶增加但 Segment 不增加 → 锁竞争加剧 |
| 2. 内存开销大 | 每个 Segment 是独立的 ReentrantLock 对象(继承 ReentrantLock 并持有独立的 table、count、modCount 等字段),16 个 Segment = 16 个 ReentrantLock + 16 个数组引用,比单纯的 Node[] 多消耗约 1KB~2KB 内存 |
| 3. 操作复杂,需要两次哈希 | JDK 1.7 定位过程:hash1 = hash(key) → segmentIndex = hash1 >>> segmentShift 定位 Segment → hash2 = hash1 & segmentMask 再次哈希定位桶。比普通 HashMap 多一次「定位 Segment」的开销 |
上图详细展示了分段锁的三大局限。关键理解:
- 并发度固定:Segment 数量在创建时确定,无法随 HashMap 扩容而增加。当桶数量从 16 增长到 65536 时,仍然只有 16 把锁,每把锁保护的桶从 1 个变成 4096 个,竞争加剧。
- 内存浪费:每个 Segment 都是一个完整的 “小 HashMap”,包含独立的数组、计数器、锁状态等,比单纯的 Node 数组多消耗不少内存。
- 两次哈希:先定位 Segment,再定位桶,增加了计算开销和代码复杂度。
二、CAS + synchronized 的优势
| 优势 | JDK 1.7 分段锁 | JDK 1.8 CAS + synchronized |
|---|---|---|
| 1. 并发度随容量动态增长 | 16 个桶 → 最多 16 线程并发 | 容量 16 时最多 16 线程并发,扩容到 65536 个桶 → 65536 线程并发,扩容后并发度自动提升,无上限 |
| 2. 锁粒度最细 | 一个 Segment 锁多个桶 | 一个锁只锁一个桶,不同桶的操作完全独立,零竞争 |
| 3. 无额外内存开销 | 需要 16 个 Segment 对象 + 16 个 ReentrantLock | 直接使用 Node[] 数组,锁的是 Node 对象本身,省去 Segment 中间层,内存更紧凑 |
| 4. 读操作完全无锁 | 读操作需要获取 Segment 的锁(虽然只是读锁) | 读操作直接访问 volatile 变量,完全无锁,零竞争,读性能大幅提升 |
上图展示了 JDK 1.8 新方案的四大优势。核心价值:
- 并发度动态增长:随着 HashMap 扩容,并发度自动提升,理论上无上限
- 锁粒度最细:只锁当前操作的桶,不同桶完全并行
- 内存更省:去掉了 Segment 中间层,直接操作 Node 数组
- 读无锁:利用 volatile 可见性,读操作完全不需要加锁
三、为什么用 synchronized 而不是 ReentrantLock?
背景:JDK 1.6 后 synchronized 的巨大优化 —— 锁升级机制:
无锁(无竞争)→ 偏向锁(单线程 CAS)→ 轻量级锁(多线程自旋 CAS)→ 重量级锁(操作系统互斥锁),低竞争时性能接近 CAS。
选择 synchronized 的原因:
| 原因 | 说明 |
|---|---|
| 1. 内存开销更小 | ReentrantLock 需要创建独立的 Lock 对象;synchronized 锁的是对象头,无额外对象 |
| 2. 自动释放锁 | ReentrantLock 必须手动 unlock(),容易忘记;synchronized 代码块结束自动释放 |
| 3. JVM 深度优化 | synchronized 是 JVM 内置锁,持续优化;ReentrantLock 是 Java 层面实现,优化空间有限 |
| 4. ConcurrentHashMap 场景特点 | 锁的是桶头节点,数量可能很多(成千上万);如果用 ReentrantLock,每个桶都需要一个 Lock 对象,内存开销无法接受 |
上图解释了为什么选择 synchronized。关键点:
- JDK 1.6 的锁升级:synchronized 不再是 “重量级锁” 的代名词,在低竞争时性能接近 CAS
- 内存开销:如果 65536 个桶都用 ReentrantLock,需要 65536 个 Lock 对象,内存开销巨大
- 自动释放:synchronized 不需要手动释放,代码更简洁,不会忘记 unlock
四、性能对比实测
测试场景一:16 线程并发 put,不同容量
| 容量 | JDK 1.7 分段锁 | JDK 1.8 CAS+synchronized |
|---|---|---|
| 16 | 基准 | +5%(小容量差异不大) |
| 1024 | 基准 | +15%(并发度提升) |
| 65536 | 基准 | +40%(大容量优势明显) |
测试场景二:读操作(8 线程并发 get)
| 版本 | 读方式 |
|---|---|
| JDK 1.7 | 需要获取 Segment 读锁,有锁竞争开销 |
| JDK 1.8 | 完全无锁,直接读 volatile,零竞争,极速读取 |
读性能提升约 20%~30%。
结论:容量越大,JDK 1.8 优势越明显。
五、源码对比
// ==================== JDK 1.7 分段锁实现(简化)==================== |
源码对比总结:
- JDK 1.7:两层结构(Segment → HashEntry),需要两次哈希,锁 Segment
- JDK 1.8:单层结构(Node[]),一次哈希,锁桶头或 CAS 无锁
面试高频追问
- 分段锁在 JDK 1.8 中完全消失了吗?
- 是的!JDK 1.8 完全移除了 Segment 类,不再有分段锁的概念
- 但
concurrencyLevel参数仍然保留,用于初始化容量估算
- synchronized 不会造成性能问题吗?
- JDK 1.6 后 synchronized 有锁升级机制,低竞争时性能很好
- ConcurrentHashMap 中锁的是桶头节点,竞争分散,很少升级到重量级锁
- CAS 失败后会怎样?
- 自旋重试!如果桶仍然为空,继续尝试 CAS
- 如果桶被其他线程占用了,则进入 synchronized 分支
常见面试变体
- “ConcurrentHashMap 在 JDK 1.7 和 1.8 有什么区别?”
- “为什么 JDK 1.8 用 synchronized 替代 ReentrantLock?”
- “分段锁有什么缺点?”
记忆口诀
分段锁三宗罪:并发度固定不随容量长、内存浪费 Segment 对象多、两次哈希定位效率低。 新方案四优势:并发度随容量涨、锁粒度细到桶、内存省去中间层、读操作完全无锁。 选 synchronized:JDK 1.6 锁升级性能好、无额外对象内存省、自动释放代码简。
总结
JDK 1.8 废弃分段锁的核心原因是 并发度固定、内存浪费、实现复杂。新的 CAS + synchronized 方案实现了 并发度动态增长(随容量扩容)、锁粒度最细(只锁单个桶)、内存更省(无 Segment 中间层)、读无锁(volatile 保证可见性)。选择 synchronized 而非 ReentrantLock 是因为 JDK 1.6 后的锁升级优化,以及无额外内存开销的优势。记住:分段锁是历史的妥协,CAS + synchronized 才是最优解。
声明:本文转载自小哈的博客 《ConcurrentHashMap 为什么在 JDK 1.8 中废弃分段锁?》,版权归原文作者所有。转载请保留本声明与原文链接。
