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

相关推荐
Thomas.Sir几秒前
第11课:文件上传下载、跨域CORS配置,SpringBoot4.x跨域新方案
java·spring boot
iCxhust4 分钟前
C#双时区时钟设计
开发语言·c#
wuminyu6 分钟前
JVM规范学习总结:class执行性能差异
java·linux·c语言·jvm·c++
安教安全员通关宝12 分钟前
2026年四川安全员B证考试题库+答案+解析
安全
sbjdhjd12 分钟前
从污点追踪断点到 PHP-FPM 请求队列:HIDS PHP WebShell 样本复盘
开发语言·网络安全·数据挖掘·渗透测试·开源·php·shell
一条泥憨鱼17 分钟前
【RabbitMQ】高级篇
java·分布式·rabbitmq·php·ruby
京师20万禁军教头25 分钟前
50-常用类-Arrays
java
寰梦26 分钟前
Spring Boot + MinIO 大文件上传完整实战:MD5 秒传、分片上传、断点续传与引用一致性
java·spring boot·文件上传·minio
SEO_juper42 分钟前
Go 站点地图生成器:从首页爬全站,一键生成 sitemap.xml
xml·开发语言·人工智能·golang·node.js·seo·外贸独立站
懂点Tech8 小时前
ctfshow misc28:GIF 高度修改解题与脚本实现
开发语言·windows·python