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 带来的额外对象开销。
代码验证内部结构
由于这是 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 的新机制。
行为差异与兼容性
升级 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 值表示特殊状态,需改为使用特定对象标识。
3. 扩容协助机制
JDK 8 扩容时,其他线程会协助迁移数据(ForwardingNode)。若调试时发现线程状态异常,可能是正在协助扩容,属正常行为。
升级迁移检查清单
在维护旧代码或进行性能调优时,建议按以下清单确认影响:
- 确认运行环境 JDK 版本:执行
java -version。如果是 1.8 及以上,ConcurrentHashMap 已不再使用 Segment 锁。 - 审查 size() 调用:搜索代码中的
.size()调用。如果在高并发写场景下依赖其返回值做精确控制,需注意弱一致性问题。 - 检查 null 值使用:检查代码中是否有
map.put(key, null)或类似逻辑,确保没有依赖 null 值作为合法业务数据。 - 性能观察:若怀疑锁竞争,可使用 JMC (Java Mission Control) 或 VisualVM 观察锁等待时间。JDK 8 下应看到 synchronized 锁等待而非 ReentrantLock 等待。
- 序列化兼容: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