文章目录

Java ConcurrentHashMap在JDK8中为什么放弃分段锁

发布于 2026-05-01 16:23:32 · 浏览 13 次 · 评论 0 条

Java ConcurrentHashMap在JDK8中为什么放弃分段锁

Java 8 对 ConcurrentHashMap 进行了彻底的重构,彻底摒弃了 Java 7 中使用的“分段锁”机制,转而采用了 CAS + synchronized 的组合方式。这一改变并非为了标新立异,而是为了解决旧设计在高并发场景下的内存与性能瓶颈。


1. 识别分段锁(Segment)的局限性

在 Java 7 中,ConcurrentHashMap 将数据分为多个段,每个段继承自 ReentrantLock。虽然这比同步整个 Map 要好,但在实际高并发生产环境中暴露出了明显的缺陷。

分析内存开销问题
每个 Segment 本质上是一个小型的 HashMap,且包含一个 ReentrantLock 对象。在默认并发级别为 16 的情况下,即使 Map 中没有数据,也会预先创建 16 个 Segment 对象和对应的锁。这种预分配机制在数据量较小或并发度需求不高时,造成了显著的内存浪费。

理解并发度瓶颈
分段锁的并发度受限于 Segment 的数量。一旦 Map 初始化,Segment 的数量就固定了(默认 16)。这意味着,理论上最多只能有 16 个线程同时进行写操作。当线程数超过 16 并且哈希碰撞严重时,线程必须排队等待锁,无法充分利用现代多核 CPU 的优势。

计算哈希定位成本
Java 7 需要进行两次哈希定位才能找到数据:

  1. 第一次 Hash 定位到具体的 Segment。
  2. 第二次 Hash 定位到 Segment 内部的 HashEntry。
    这种双重定位增加了 CPU 的计算开销,降低了访问速度。

2. 掌握 JDK 8 的核心优化策略

Java 8 放弃了 Segment,转而使用 Node 数组 + 链表 + 红黑树的结构。锁的粒度从“段”细化到了“桶”级别,即数组中的每一个 Node 元素。

使用 CAS 进行无锁尝试
在执行插入操作时,如果当前桶为空,JDK 8 会利用 Unsafe 类的 CAS (Compare And Swap) 指令尝试直接插入。

  1. 检查当前桶位置是否为 null。
  2. 执行 CAS 操作将新节点放入。
    这一过程完全不需要加锁,是纯硬件级别的原子操作,速度极快。

启用 synchronized 作为后备锁
只有当 CAS 操作失败(即发生哈希冲突,桶不为空)时,才会使用 synchronized 锁住当前桶的头节点。

  1. 锁定当前桶的第一个节点。
  2. 遍历链表或红黑树进行更新或插入。

这解决了两个问题:

  • 锁粒度极细:只锁住当前操作的一个桶,不影响其他桶的并发读写。
  • JVM 优化:现代 JDK 对 synchronized 进行了大量优化(偏向锁、轻量级锁、锁消除),在低竞争下性能几乎等同于无锁,且比 ReentrantLock 占用更少的内存。

3. 执行插入操作的逻辑流程

为了直观理解 Java 8 如何处理并发写入,查看以下逻辑流程:

graph TD A["Start: Calculate Hash"] --> B{Bucket is null?} B -- Yes --> C["CAS Insert Node"] C --> Success{CAS Success?} Success -- Yes --> G["End: Operation Complete"] Success -- No --> D["synchronized Lock Head Node"] B -- No --> D D --> E{Is Linked List or Red-Black Tree?} E -- Linked List --> F["Traverse List: Update or Insert"] E -- Tree --> H["Traverse Tree: Update or Insert"] F --> I["Check Tree Threshold: >= 8?"] I -- Yes --> J["Convert to Red-Black Tree"] I -- No --> G H --> G J --> G

4. 对比新旧方案的差异

对照下表,清晰地看到 Java 8 相对于 Java 7 的具体提升:

特性维度 JDK 1.7 (分段锁 Segment) JDK 8 (CAS + synchronized)
数据结构 Segment 数组 + HashEntry 数组 Node 数组 + 链表 / 红黑树
锁粒度 Segment(一组桶,较粗) 单个桶的头节点(极细)
并发度 固定,等于 Segment 数量 动态,最大等于数组长度
锁机制 ReentrantLock CAS(无锁)+ synchronized(后备)
内存开销 较大(每个 Segment 都是对象) 较小(结构更紧凑,无额外锁对象)
查询性能 需两次 Hash 定位 需一次 Hash 定位,链表转树优化

5. 理解辅助性能的改进细节

除了锁机制的变更,Java 8 还引入了针对极端情况的优化措施,以确保在各种数据规模下都能保持高性能。

优化哈希冲突处理
当同一个桶内的元素数量过多时(超过阈值 8),链表的查询效率会从 $O(1)$ 退化为 $O(n)$。

  1. 监测桶内节点数量。
  2. 转换链表为红黑树。
    红黑树的查询时间复杂度为 $O(\log n)$,这在数据量巨大或 Hash 算法不理想导致严重碰撞时,能有效防止性能断崖式下跌。

提升扩容效率
在 Java 7 中,扩容涉及对 Segment 的整体操作,较为笨重。Java 8 支持多线程协助扩容。

  1. 检测到扩容标记。
  2. 分配任务给当前线程,负责迁移部分桶的数据。
    其他正在写入的线程如果检测到正在扩容,也会参与迁移工作,极大缩短了扩容导致的“卡顿”时间。

降低内存占用与 GC 压力
由于移除了 Segment 这种重量级对象,Map 的元数据开销大幅降低。对象数量的减少直接减轻了垃圾回收(GC)的压力,特别是在堆内存紧张的大型应用中,这一改进对系统的稳定性至关重要。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文