ConcurrentHashMap 线程安全机制与 JDK 1.8 演进

ConcurrentHashMap 的核心设计目标是:在保证线程安全的前提下,尽可能缩小锁的作用范围、降低锁竞争,从而提升并发读写性能。不同 JDK 版本的实现思路存在本质差异。


一、JDK 1.7:分段锁(Segment)实现

1. 底层结构

采用 Segment 数组 + HashEntry 数组 + 链表 的两级哈希架构:

  • Segment 继承自 ReentrantLock,每个 Segment 是一个独立的重入锁,保护其内部的 HashEntry 数组与链表
  • 默认初始化 16 个 Segment,即默认并发度为 16,且扩容时 Segment 数量不变
  • 每个 Segment 内部维护独立的哈希数组、计数器和扩容阈值,彼此完全隔离

2. 线程安全保障

  • 写操作:仅对当前 key 所属的单个 Segment 加锁,其他 Segment 的读写完全不受影响,实现分段并发
  • 读操作 :HashEntry 的 value 字段用 volatile 修饰,保证内存可见性,读操作全程无锁
  • 扩容:仅在单个 Segment 内执行单线程扩容,不会阻塞其他 Segment 的操作

3. 固有局限

  • 锁粒度为「段级」,并发上限固定为 Segment 数量,无法随哈希数组扩容同步提升
  • 哈希冲突严重时,单个 Segment 内链表过长,查询性能线性退化
  • Segment 本身作为锁对象,带来额外的内存开销

二、JDK 1.8:CAS + synchronized + volatile 实现

JDK 1.8 彻底重构了底层实现,将锁粒度细化到单个哈希桶,并发性能大幅提升,是目前工业界的主流版本。

1. 底层结构

采用 Node 数组 + 链表 + 红黑树 的单级哈希结构:

  • 移除了 Segment 分段层,直接用 Node<K,V>[] table 作为核心哈希数组
  • 引入红黑树优化:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转换为红黑树,将冲突场景下的查询复杂度从 O (n) 降至 O (logn)
  • 扩容过程中使用 ForwardingNode 占位旧桶,标识该桶已完成迁移

2. 线程安全的三大核心保障

(1)可见性:volatile 全域覆盖

这是读操作可以无锁执行的基础:

  • table 数组本身用 volatile 修饰,保证数组引用变更的内存可见性
  • Node 节点的 val(存储值)和 next(后继指针)均用 volatile 修饰,保证节点数据变更的可见性
(2)写原子性:分场景的同步策略

写操作(put/remove 等)根据桶的状态选择不同的同步方式,兼顾性能与安全:

  1. 空桶插入 :如果目标桶位置为 null,直接通过 CAS 原子操作 插入新节点,全程无锁
  2. 桶内操作 :如果目标桶已有节点(链表或红黑树),使用 synchronized 锁定该桶的头节点 (链表头节点 / 红黑树容器 TreeBin)
    • 锁粒度从「段级」细化到「单个哈希桶」,并发度等于数组长度,且随扩容同步提升
  3. 扩容中状态 :如果遇到 ForwardingNode 占位节点,说明该桶正在迁移,当前线程会主动协助参与扩容
(3)读操作:几乎全程无锁

get 方法在绝大多数场景下不需要加锁:

  1. 计算 hash 定位到桶,直接读取头节点数据
  2. 遍历链表或红黑树查找,完全依赖 volatile 保证可见性
  3. 仅当遇到扩容中的 ForwardingNode 时,才会自动转发到新数组中继续查找
  4. 弱一致性:迭代器和 get 均为弱一致语义,不保证实时读到最新写入的数据

3. 并发扩容机制

JDK 1.8 最核心的优化之一是多线程并发全局扩容,进一步加快性能

  • 扩容时,旧数组的桶按批次迁移到新数组,每个桶迁移完成后用 ForwardingNode 占位
  • 多个业务线程可以同时参与扩容,通过 CAS 分配各自负责的桶区间,充分利用多核算力
  • 迁移过程中,写操作会协助扩容,读操作会自动转发到新表,业务线程几乎无阻塞

三、JDK 1.8 相对 1.7 的核心变化总结

表格

对比维度 JDK 1.7 JDK 1.8
底层结构 Segment 数组 + HashEntry 数组 + 链表 Node 数组 + 链表 + 红黑树
锁机制 分段 ReentrantLock CAS + synchronized
锁粒度 段级(默认 16 段) 桶级(单个哈希桶)
并发度 固定为 Segment 数量 等于数组长度,扩容同步提升
扩容方式 单线程段内扩容 多线程并发全局扩容
计数方式 遍历所有 Segment 求和,并发误差大 baseCount + CounterCell 分段计数,类似 LongAdder 思想
查询性能 链表遍历,冲突严重时线性退化 红黑树优化,稳定 O (logn)
内存开销 大量 Segment 锁对象,开销高 无额外锁对象,内存更紧凑
插入方式 头插法 尾插法(需统计链表长度,方便转换红黑树)

四、关键细节补充

1. 为什么用 synchronized 替代 ReentrantLock?

  • JDK 1.8 对 synchronized 做了深度优化(偏向锁 → 轻量级锁 → 重量级锁的自适应升级),低竞争场景下性能优于 ReentrantLock
  • synchronized 是 JVM 内置锁,不需要在 Java 堆中维护额外的锁对象,内存开销更低
  • 锁粒度细化到桶级后,单个锁的持有时间极短,synchronized 的性能优势更明显

2. 为什么不允许 key/value 为 null?

与 HashMap 不同,ConcurrentHashMap 禁止 key 或 value 为 null,核心原因是并发场景下会产生歧义:无法区分「值本身就是 null」和「key 不存在」,避免并发读写时的语义混乱。

相关推荐
tqs_1234519 分钟前
值传递与引用传递
java·开发语言·python
2601_9621819622 分钟前
Spring boot从0到1 - day01
java·spring boot·后端
一直C28 分钟前
Linux系统编程|信号进阶 + SystemV IPC(消息队列、共享内存)
linux·c语言·开发语言·算法·青少年编程·vim
典典分享指南36 分钟前
短视频分镜提示词:让 AI 出片的产品口播
java·c#·bash·composer·symfony
IT_Octopus38 分钟前
G1 与 GC 日志入门课程(以本次离线菜单 Full GC 事故为教材)
java·jvm
小王C语言42 分钟前
QT 基础:QT SDK 下载、配置、新建项目、项目代码解释
开发语言·qt
cfm_29141 小时前
ReentrantLock 中 lock 与 tryLock 核心区别
java
.Hypocritical.1 小时前
Tomcat本地部署+远程服务器部署超详细教程
java·服务器·tomcat
2601_962062941 小时前
Spring Boot入门——Spring Boot项目的创建
java·数据库·spring boot