ConcurrentHashMap 为什么在 JDK 1.8 中废弃分段锁?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/

面试考察点

面试官提出这个问题,通常希望考察你以下几个层面的理解:

  1. ConcurrentHashMap 演进历史的了解:你是否清楚其在不同 JDK 版本中的重大改进。
  2. 对并发控制技术选型的深度理解:不仅仅是知道 “用了什么”,更要理解 “为什么用这个,而不用那个” 背后的设计权衡。
  3. 对技术方案优劣的批判性分析能力:能否清晰地分析分段锁(Segment Lock)方案存在的固有缺陷,以及新方案(CAS + synchronized)如何针对性解决这些问题。
  4. 将并发理论与实际数据结构结合的能力:如何将锁粒度、内存开销、并发度等理论概念,映射到 ConcurrentHashMap 这个具体容器的实现细节上。

核心答案

在 JDK 1.8 中,ConcurrentHashMap 废弃了基于 Segment 的分段锁机制,改为采用 Node 数组 + CAS + synchronized 的实现方式。主要原因是:分段锁的并发度受限于 Segment 的数量,且内存开销较大,而在新的方案下,锁的粒度被细化到了单个桶(Node)级别,理论上并发度与数组长度相同,能实现更高的并行性能,同时数据结构变得更简洁,内存利用更高效。

深度解析

原理/机制:JDK 1.7 分段锁的局限性

  1. 并发度固定,扩展性差

    • 原理:在 JDK 1.7 中,ConcurrentHashMap 内部由一个 Segment 数组组成,每个 Segment 本质上是一个独立的 ReentrantLock 和一个小型的 HashMap。锁的粒度是 Segment
    • 问题:并发度(即最多可同时执行的写操作数)在创建时就固定了(默认 16),且无法动态扩容。即使数据均匀分布,在极端高并发场景下,对同一个 Segment 的访问仍会成为瓶颈。
  2. 内存开销与访问开销

    • 原理:每次访问都需要进行两次哈希定位(先定位 Segment,再定位桶),性能有一定损耗。同时,每个 Segment 都继承了 ReentrantLock,本身是一个重量级对象,存在额外的内存开销。
    • 问题:在小数据量或并发竞争不激烈时,这种固定开销显得不划算。

JDK 1.8 新方案的改进

  1. 更细粒度的锁

    • 原理:数据结构回归与 HashMap 相似的 Node 数组 + 链表/红黑树。锁的粒度从 “段” 细化到了 “桶的头节点”。
    • 优势:理论上,只要线程操作的是不同的桶,就完全不会发生锁竞争。并发度上限等于数组长度,可以动态扩容,远高于固定的 Segment 数量。
  2. 利用现代 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是桶的头节点
          // ... 在同步块内进行链表或红黑树的插入/更新
      }
      
  3. 使用内置的 synchronized 作为冲突后备锁

    • 原理:当 CAS 失败(发生哈希冲突)时,只会对当前冲突的桶的头节点使用 synchronized 进行加锁。
    • 优势
      • 锁粒度极小:仅锁一个桶。
      • JVM 持续优化synchronized 在 JDK 1.6 后引入了偏向锁、轻量级锁、自旋锁、锁消除、锁粗化等大量优化,其性能在低竞争场景下已非常优秀,不再像早期版本那样 “重量级”。
      • 减少依赖:不再需要依赖 ReentrantLock,减少了 API 层面的复杂性。

对比分析与最佳实践

特性维度JDK 1.7 (分段锁)JDK 1.8 (CAS + synchronized)
数据结构Segment 数组 + HashEntry 数组Node 数组 + 链表/红黑树
锁粒度Segment(一组桶)单个桶的头节点
并发度固定,创建时决定可扩展,与数组大小相关
锁机制基于 ReentrantLockCAS(无锁) + synchronized(后备锁)
内存开销较大(每个Segment都是对象+锁)较小(数据结构更紧凑)
Hash冲突处理链表链表 -> 红黑树(优化长链表查询)

最佳实践与常见误区

  • 最佳实践:JDK 1.8 后的 ConcurrentHashMap 实现是通用的高性能选择。在开发中,应优先使用最新稳定版 JDK 提供的实现,无需再关心分段锁的概念。
  • 常见误区:认为 synchronized 一定比 ReentrantLock 性能差。在 ConcurrentHashMap 这种 锁竞争时间极短、范围极小 的场景下,经过深度优化的 synchronized 因其 JVM 原生支持、开销低 的特性,反而是更优选择。ReentrantLock 的优势在于 可中断、可超时、公平锁、条件变量 等高级功能,而这些在 ConcurrentHashMap 的桶锁场景中并非必需。

总结

JDK 1.8 废弃分段锁,是为了追求极致的并发性能和更低的内存开销,通过 “无锁 CAS 尝试 + 极细粒度 synchronized 后备” 的组合策略,将锁竞争的概率和范围降到了最低,这是对硬件特性和 JVM 锁优化成果的完美运用。