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

相关推荐
Sarvartha4 小时前
final 关键字
java·开发语言
2601_952047794 小时前
单一策略深度教程:用轻易云把集成任务的报错实时推送到钉钉机器人
java·机器人·钉钉
程序员Sunday5 小时前
JavaScript 事件循环面试题,宏任务与微任务怎么执行|Sunday面试指南
开发语言·javascript·面试·校招·事件循环·程序员sunday
数据狐(Datafox)5 小时前
淘宝图片搜索 API 落地实战:基于以图搜货搭建跨境电商选品系统
java·大数据·微服务
zhangzeyuaaa6 小时前
Ruby 多线程、GVL(GIL)与 Mutex 完全指南
开发语言·前端·ruby
谢亮_vipxieliang7 小时前
用 PHP 构建轻量级 REST API:从路由到鉴权的完整实践
开发语言·后端·php
Escalating_xu7 小时前
【C 语言】数据在内存中的存储:补码、大小端、整型陷阱与 IEEE 754 全解析
java·c语言·网络
思无邪667 小时前
用 AI 做 JS 逆向:从抓包到复现的完整方法论
开发语言·javascript·人工智能