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 需要进行两次哈希定位才能找到数据:
- 第一次 Hash 定位到具体的 Segment。
- 第二次 Hash 定位到 Segment 内部的 HashEntry。
这种双重定位增加了 CPU 的计算开销,降低了访问速度。
2. 掌握 JDK 8 的核心优化策略
Java 8 放弃了 Segment,转而使用 Node 数组 + 链表 + 红黑树的结构。锁的粒度从“段”细化到了“桶”级别,即数组中的每一个 Node 元素。
使用 CAS 进行无锁尝试
在执行插入操作时,如果当前桶为空,JDK 8 会利用 Unsafe 类的 CAS (Compare And Swap) 指令尝试直接插入。
- 检查当前桶位置是否为 null。
- 执行 CAS 操作将新节点放入。
这一过程完全不需要加锁,是纯硬件级别的原子操作,速度极快。
启用 synchronized 作为后备锁
只有当 CAS 操作失败(即发生哈希冲突,桶不为空)时,才会使用 synchronized 锁住当前桶的头节点。
- 锁定当前桶的第一个节点。
- 遍历链表或红黑树进行更新或插入。
这解决了两个问题:
- 锁粒度极细:只锁住当前操作的一个桶,不影响其他桶的并发读写。
- JVM 优化:现代 JDK 对
synchronized进行了大量优化(偏向锁、轻量级锁、锁消除),在低竞争下性能几乎等同于无锁,且比ReentrantLock占用更少的内存。
3. 执行插入操作的逻辑流程
为了直观理解 Java 8 如何处理并发写入,查看以下逻辑流程:
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)$。
- 监测桶内节点数量。
- 转换链表为红黑树。
红黑树的查询时间复杂度为 $O(\log n)$,这在数据量巨大或 Hash 算法不理想导致严重碰撞时,能有效防止性能断崖式下跌。
提升扩容效率
在 Java 7 中,扩容涉及对 Segment 的整体操作,较为笨重。Java 8 支持多线程协助扩容。
- 检测到扩容标记。
- 分配任务给当前线程,负责迁移部分桶的数据。
其他正在写入的线程如果检测到正在扩容,也会参与迁移工作,极大缩短了扩容导致的“卡顿”时间。
降低内存占用与 GC 压力
由于移除了 Segment 这种重量级对象,Map 的元数据开销大幅降低。对象数量的减少直接减轻了垃圾回收(GC)的压力,特别是在堆内存紧张的大型应用中,这一改进对系统的稳定性至关重要。

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