ConcurrentHashMap 在 JDK1.8 中废弃了 Segment 分段锁为什么?

文章导读
JDK 1.8 废弃 Segment 分段锁主要是为了降低内存开销并提升高并发下的吞吐量,将锁粒度从“段”细化到“桶节点”,适合追求更高并发性能且无需强一致 size() 计数的场景。
📋 目录
  1. A 核心原理变更
  2. B 代码验证内部结构
  3. C 行为差异与兼容性
  4. D 升级迁移检查清单
  5. E 参考资料
A A

JDK 1.8 废弃 Segment 分段锁主要是为了降低内存开销并提升高并发下的吞吐量,将锁粒度从“段”细化到“桶节点”,适合追求更高并发性能且无需强一致 size() 计数的场景。

先说结论:JDK 1.8 改用 CAS + synchronized 锁单个桶头节点,解决了分段锁并发度固定和内存浪费问题,但 size() 变为弱一致性计数。

  • 适合:高并发读写场景,尤其是写操作频繁且 Key 分布较均匀的业务。
  • 重点看:锁粒度从 Segment 变为 Node,内存占用显著降低,扩容机制更灵活。
  • 别忽略:size() 方法不再强一致,依赖精确计数的业务需配合其他机制验证;JDK 7 与 8 均不支持 null 键值对。

核心原理变更

JDK 1.7 及之前的 ConcurrentHashMap 采用 Segment 分段锁,内部是一个 Segment 数组,每个 Segment 包含一个 ReentrantLock 和一个 HashEntry 数组。这种设计有两个主要瓶颈:一是并发度受限于 Segment 数量(默认 16),无法动态扩容;二是每个 Segment 都是独立对象,内存开销大,且访问时需要两次哈希定位。

JDK 1.8 彻底移除了 Segment 结构,数据结构回归到与 HashMap 相似的 Node 数组 + 链表/红黑树。锁机制变为 CAS + synchronized,锁粒度细化到单个桶的头节点。当桶为空时,通过 CAS 无锁插入;当发生哈希冲突时,仅锁住该桶的头节点。这种变化使得理论并发度上限等于数组长度,且能动态扩容。同时,JVM 对 synchronized 的优化(如锁消除、锁粗化)在细粒度场景下表现更好,减少了 ReentrantLock 带来的额外对象开销。

ConcurrentHashMap 在 JDK1.8 中废弃了 Segment 分段锁为什么?

代码验证内部结构

由于这是 JDK 内部实现,无法通过配置开关验证,但可通过反射查看内部字段确认当前运行环境的具体结构:

import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentHashMap;

public class CHMStructureCheck {
    public static void main(String[] args) throws Exception {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        Class<?> clazz = map.getClass();
        
        // 尝试获取 JDK 7 的 segments 字段
        try {
            Field segmentsField = clazz.getDeclaredField("segments");
            System.out.println("当前版本包含 segments 字段,可能是 JDK 7 或更早");
        } catch (NoSuchFieldException e) {
            System.out.println("当前版本无 segments 字段");
        }

        // 尝试获取 JDK 8 的 table 字段
        try {
            Field tableField = clazz.getDeclaredField("table");
            tableField.setAccessible(true);
            Object table = tableField.get(map);
            System.out.println("当前版本包含 table 字段 (Node[]), 确认为 JDK 8+ 结构");
        } catch (NoSuchFieldException e) {
            System.out.println("当前版本无 table 字段");
        }
    }
}

若代码输出包含table字段且无segments字段,说明已生效 JDK 8 的新机制。

ConcurrentHashMap 在 JDK1.8 中废弃了 Segment 分段锁为什么?

行为差异与兼容性

升级 JDK 版本时,除了性能变化,还需关注以下行为差异:

1. size() 弱一致性计数

JDK 8 中 size() 基于 baseCount + CounterCell[] 计算,高并发写入时可能滞后。公开资料中没有看到可靠的量化数据说明滞后具体毫秒数,但设计上是最终一致。不要用它做严格的流控条件。

// 验证 size() 弱一致性示例
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
// 多线程并发 put 后,立即调用 size() 可能小于实际插入总数
// 建议业务层维护原子计数器,或使用 mappingCount() 获取 long 类型估算值

2. null 键值对限制

技术修正:JDK 7 和 JDK 8 的 ConcurrentHashMap 均不允许 null 键值对,都会抛出 NullPointerException。迁移旧代码时,若发现旧代码依赖 null 值表示特殊状态,需改为使用特定对象标识。

ConcurrentHashMap 在 JDK1.8 中废弃了 Segment 分段锁为什么?

3. 扩容协助机制

JDK 8 扩容时,其他线程会协助迁移数据(ForwardingNode)。若调试时发现线程状态异常,可能是正在协助扩容,属正常行为。

升级迁移检查清单

在维护旧代码或进行性能调优时,建议按以下清单确认影响:

  1. 确认运行环境 JDK 版本:执行java -version。如果是 1.8 及以上,ConcurrentHashMap 已不再使用 Segment 锁。
  2. 审查 size() 调用:搜索代码中的.size()调用。如果在高并发写场景下依赖其返回值做精确控制,需注意弱一致性问题。
  3. 检查 null 值使用:检查代码中是否有map.put(key, null)或类似逻辑,确保没有依赖 null 值作为合法业务数据。
  4. 性能观察:若怀疑锁竞争,可使用 JMC (Java Mission Control) 或 VisualVM 观察锁等待时间。JDK 8 下应看到 synchronized 锁等待而非 ReentrantLock 等待。
  5. 序列化兼容:JDK 8 改变了序列化方式(不再序列化 Segment),旧版本 serialized 数据可能无法直接在新版本读取,需注意版本兼容性。

参考资料

  • Oracle Official Documentation: Java Platform, Standard Edition 8 API Specification - ConcurrentHashMap
  • Doug Lea: JSR-166 Concurrent Utilities
  • OpenJDK Source Code: java.util.concurrent.ConcurrentHashMap