彻底讲透 JDK1.8 ConcurrentHashMap 线程安全原理
2026/9/13 22:51:48 网站建设 项目流程

彻底讲透 JDK1.8 ConcurrentHashMap 线程安全原理(全网最通俗图文完整版)

前言

HashMap 线程不安全,多线程并发 put 会出现数据覆盖、数据丢失;JDK1.7 还会出现扩容死循环。为了解决并发安全问题,JDK1.8 的 ConcurrentHashMap 抛弃了 JDK1.7 的 Segment 分段锁,采用volatile + CAS + synchronized + 并发扩容四大机制实现高性能线程安全。

本文从零梳理底层结构、每一个机制的作用位置、适用场景、图文对照,彻底搞懂 CHM 如何保证并发安全。

一、底层数据结构总览(图文)

JDK1.8 ConcurrentHashMap 底层结构:数组 + 链表 + 红黑树

1.1 普通链表桶结构(无树化)

【volatile 数组引用】 transient volatile Node<K,V>[] table ​ 数组堆内存结构: table[0] → null table[1] → 🔒Node1(桶头节点 / synchronized 锁对象) ↓ next(volatile) Node2 ↓ next(volatile) Node3 ​

核心规则:

  • 数组中只有桶头 Node的引用存在 table 数组格子中

  • 链表后续 Node2、Node3不在数组中,仅靠 Node.next 指针串联

  • 所有 Node 对象全部创建在堆内存

1.2 红黑树桶结构(树化后)

table[2] → 🔒TreeBin(桶头、synchronized 锁对象,继承 Node) ├─ root → TreeNode(红黑树根节点,❗不是桶头) ├─ 挂载大量 TreeNode 左右子节点 └─ 内置读写锁,提升并发读性能 ​

重中之重(90%人混淆):

  • 树化后,数组桶头不是 TreeNode,而是TreeBin

  • TreeNode 只是内部数据节点,永远不会被锁

  • synchronized 锁的是桶头 TreeBin 对象

二、volatile 两层作用(核心难点彻底拆解)

ConcurrentHashMap 有两套独立的 volatile 机制,互不干扰,这是最容易混淆的知识点。

2.1 第一层:volatile 修饰 table 数组引用

transient volatile Node<K,V>[] table;

作用:保证「数组引用切换」的可见性

关键点纠正:

  • volatile不修饰数组内容、不修饰数组元素

  • 仅修饰table 这个变量(保存数组堆地址)

  • 数组一旦 new 出来,自身堆地址永远不变

  • volatile 只负责:扩容/初始化时 table = 新数组地址全局可见

场景

  • 首次 put 初始化数组

  • 扩容结束切换新数组table = nextTable

保证所有线程立刻识别新数组,不会继续读写旧数组,避免数据错乱。

2.2 第二层:volatile 修饰 Node 内部字段

static class Node<K,V> { final int hash; final K key; volatile V val; // 值可见性 volatile Node<K,V> next; // 指针可见性 }

作用:保证节点数据、链表结构的可见性

  • val volatile:一个线程修改 value,其他线程立刻读到最新值

  • next volatile:线程追加新节点、修改链表指针,其他遍历线程立刻感知链表变化

核心价值:支撑get() 无锁读取,全程不加锁,依靠 volatile 可见性拿到最新数据,并发读性能极高。

2.3 补充:数组格子可见性(Unsafe 兜底)

table 的 volatile 不管数组内部元素修改,所以 JDK 底层通过三个 Unsafe 方法实现数组桶位的 volatile 读写:

  • tabAt:volatile 读桶头数据

  • casTabAt:CAS 无锁更新桶位

  • setTabAt:volatile 写桶位数据

三、CAS 无锁机制(空桶写入专属)

3.1 核心定位

CAS 是无锁乐观操作,不是加锁!

适用唯一场景:桶位为 null(空桶)

// 如果当前桶为空,CAS 尝试写入新节点 casTabAt(tab, i, null, new Node<>(hash, key, value, null));

3.2 执行逻辑

  • 多线程同时写入同一个空桶

  • CAS 比较内存值:预期是 null 才写入

  • 只有一个线程成功,其余线程自旋重试

空桶场景无锁竞争,性能极致,避免了无脑加锁。

四、synchronized 细粒度锁(冲突场景核心保障)

当桶位已有数据(链表/红黑树),CAS 无法解决并发冲突,启用 synchronized 互斥锁。

4.1 锁对象核心规则(全网最清晰总结)

  • 链表桶:锁「桶头普通 Node 节点」

  • 红黑树桶:锁「桶头 TreeBin 对象」

  • ❌ 不锁链表所有节点、❌ 不锁 TreeNode、❌ 不锁数组

4.2 如何实现锁住整条桶数据?

并不是语法上锁了整条链表/红黑树,而是并发约定

所有线程修改同一个桶的数据,必须先竞争桶头锁,同一时刻只有一个线程能修改该桶,从而保证整条链表/红黑树的线程安全。

4.3 锁粒度优势

  • 不同桶互不阻塞,支持最大并发

  • 废弃 JDK1.7 笨重的 Segment 分段锁

  • JDK1.8 对 synchronized 做了大量优化(偏向锁/轻量级锁),性能远超 ReentrantLock

五、并发扩容机制(兜底线程安全)

CHM 扩容不会阻塞所有线程,支持多线程协助扩容,是高并发核心亮点。

5.1 核心流程

  1. 达到扩容阈值,新建 nextTable 新数组

  2. 原桶正在迁移时,标记为ForwardingNode

  3. 其他线程写入时发现桶是 ForwardingNode,先协助扩容,迁移完成再写入

  4. 全部迁移完毕,执行table = nextTable(volatile 全局可见)

彻底解决了 HashMap 扩容死循环、数据丢失问题。

六、完整 put 执行流程(串联所有机制)

  1. 判断数组是否未初始化,CAS 初始化数组(volatile 保证数组引用可见)

  2. 计算 hash 定位桶下标,通过 tabAt volatile 读获取最新桶头

  3. 桶为空:CAS 无锁写入新节点

  4. 桶正在扩容:协助扩容后再写入

  5. 桶已有数据:synchronized 锁桶头

    • 链表:遍历覆盖/追加节点,依靠 node val/next volatile 保证可见性

    • 链表长度≥8 且数组≥64:转为红黑树

  6. 更新元素数量,判断是否触发扩容

七、最终总结(可直接背诵、发博客收尾)

JDK1.8 ConcurrentHashMap 依靠三层可见性 + 无锁CAS + 细粒度锁 + 并发扩容全方位保证线程安全:

  1. volatile 两层可见性:table 数组引用保证扩容切换全局可见;Node 的 val、next 保证节点数据和链表结构实时可见,支撑无锁读。

  2. CAS 无锁写入:空桶场景乐观写入,无锁高并发,避免多余锁竞争。

  3. synchronized 细粒度互斥:冲突场景锁桶头(Node/TreeBin),仅阻塞同桶线程,最大化并发性能。

  4. 并发扩容机制:多线程协助迁移,扩容不阻塞读写,彻底解决并发扩容数据异常问题。

相比 JDK1.7 分段锁,锁粒度更细、并发度更高、读写性能更优,是 Java 高并发场景下的标准键值存储方案。

(注:部分内容可能由 AI 生成)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询