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 不存在」,避免并发读写时的语义混乱。

相关推荐
SunnyDays101111 小时前
Java 拆分 Word 文档教程:按页、分页符和分节符拆分
java·word·分页·拆分
彧azz11 小时前
Java学习记录:判断语句
java·笔记·学习·算法
企业数字化笔记11 小时前
一批相同的电脑怎么建账?数量管理、单件编码和拆分规则
java·后端·电脑
君顾111 小时前
本地城市社区家政物业联动系统源码:架构设计与派单联动实战
java·开发语言·健身房
hai_android11 小时前
Android 组件化开发实践
android·java·kotlin
MayBaymax11 小时前
MongoDB 基础概念
java·数据库·mongodb
jimmyleeee11 小时前
大模型安全之二十一:构建 AI 安全控制栈:从威胁映射到持续合规的完整指南
人工智能·安全
AI编码进化论11 小时前
云IDE介绍:环境模板化、Agent上云,云IDE该怎么选
开发语言·ide·人工智能·团队开发·ai编程
晴天的雨.99211 小时前
[C++算法]盛最多水的容器(双指针算法)
开发语言·c++·算法
泡海椒11 小时前
jquick-pdf 文本元素实战:段落、行内文本、制表符用法详解
java·开发语言·pdf