Java8 ConcurrentHashMap 深度解析(底层数据结构详解及方法执行流程)

Java8 ConcurrentHashMap 深度解析(底层数据结构详解及方法执行流程)

一、数据结构 (Data Structure)

Java 8 的 ConcurrentHashMap 摒弃了 Java 7 中的 Segment 分段锁 机制,采用了与 HashMap 1.8 类似的 数组 + 链表 + 红黑树 的结构,但在并发控制上做了特殊设计。

1. 核心结构图
ConcurrentHashMap ├── Node[] table (volatile) // 哈希桶数组 │ ├── Node (链表节点) │ ├── TreeBin (红黑树节点包装) │ └── ForwardingNode (扩容迁移标志) ├── Node[] nextTable // 扩容时的新数组 ├── LongAdder baseCount // 基础计数 └── CounterCell[] counterCells // 并发计数单元格 (解决热点竞争)
2. 关键节点类型
  • Node: 存储键值对的基本节点,包含 key, val, hash, next
  • TreeBin: 当链表长度超过阈值(默认 8)且数组长度超过 64 时,链表转为红黑树。TreeBin 是红黑树的根节点包装,不直接存储数据,而是维护树的平衡。
  • ForwardingNode: 专门用于扩容。当某个桶正在被迁移时,该位置会放置一个 ForwardingNode,其 hash 值为 MOVED (-1),指向新的数组 nextTable,引导后续请求去新数组操作。
3. 并发控制核心
  • CAS (Compare-And-Swap): 用于无锁插入节点(如数组初始化、空桶插入)。
  • synchronized: 当发生哈希冲突(链表或树操作)时,只锁定当前桶的 头节点 (Head Node)。粒度更细,性能优于 Java 7 的 Segment 锁。
  • volatile: table 数组和 Nodevalnext 指针都是 volatile 的,保证可见性。

二、put() 方法执行流程

putValput 的核心实现,流程较为复杂,主要步骤如下:

1. 参数检查与 Hash 计算
  • 检查 key 和 value 是否为 null(CHM 不允许 null)。
  • 计算 key 的 hash 值(使用扰动函数,高位参与运算,减少冲突)。
2. 数组初始化 (initTable)
  • 如果 table 为 null 或长度为 0,调用 initTable()
  • 使用 CAS 竞争初始化权。如果当前线程发现 sizeCtl < 0,说明其他线程正在初始化,当前线程让出 CPU (Thread.yield()) 自旋等待。
  • 初始化成功后,设置 sizeCtl 为扩容阈值(通常为容量的 0.75)。
3. 定位桶位置
  • 根据 (n - 1) & hash 计算数组下标 i
  • 获取该位置的节点 f
4. 插入逻辑 (核心分支)

这里分为三种情况:

  • 情况 A:桶为空 (f == null)
    • 使用 CAS 尝试将新节点插入到该位置。
    • 如果 CAS 成功,直接结束。
    • 如果 CAS 失败(说明有其他线程竞争),进入自旋重试。
  • 情况 B:正在扩容 (f.hash == MOVED)
    • 说明当前桶的节点是 ForwardingNode
    • 调用 helpTransfer(),当前线程会 协助扩容,将旧数组的数据迁移到新数组 nextTable 中。
  • 情况 C:桶不为空且未扩容 (哈希冲突)
    • 使用 synchronized (f) 锁定该桶的头节点。
    • 再次检查 头节点是否变化(双重检查)。
    • 链表插入: 遍历链表。
      • 如果找到相同 key,根据 onlyIfAbsent 决定是否覆盖 value。
      • 如果没找到,尾插法插入新节点。
      • 插入后检查链表长度,如果 >= 8,调用 treeifyBin 尝试转为红黑树(需数组长度 >= 64,否则先扩容)。
    • 红黑树插入: 如果 fTreeBin 类型,调用 putTreeVal 插入树节点。
    • 解锁。
5. 计数与扩容检查
  • 调用 addCount(1L, binCount) 更新元素个数。
  • 检查是否达到扩容阈值,如果达到,触发 transfer() 进行扩容(容量翻倍)。
public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { // key 和 value 不能为空 if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { // f = 目标位置元素 Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值 if (tab == null || (n = tab.length) == 0) // 数组桶为空,初始化数组桶(自旋+CAS) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; // 使用 synchronized 加锁加入节点 synchronized (f) { if (tabAt(tab, i) == f) { // 说明是链表 if (fh >= 0) { binCount = 1; // 循环加入新的或者覆盖节点 for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { // 红黑树 Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }

三、get() 方法执行流程

get 操作是 无锁 的,这是 CHM 高性能的关键之一。

1. 计算 Hash 与定位
  • 计算 key 的 hash 值。
  • 定位数组下标 i = (n - 1) & hash
  • 获取该位置的第一个节点 tab[i]
2. 遍历查找
  • 匹配头节点: 如果头节点的 hash 和 key 匹配,直接返回 value。
  • 链表遍历: 如果不匹配且 next 不为 null,遍历链表查找。
  • 红黑树查找: 如果节点类型是 TreeBin,调用 find 方法在红黑树中查找。
  • 扩容处理: 如果节点是 ForwardingNode,说明正在扩容,需要在 nextTable (新数组) 中继续查找。
3. 返回结果
  • 找到则返回 value,找不到返回 null。
  • 注意: 整个过程中没有使用任何锁,依靠 volatile 保证读取到最新的数据。
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; // key 所在的 hash 位置 int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 如果指定位置元素存在,头结点hash值相同 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) // key hash 值相等,key值相同,直接返回元素 value return e.val; } else if (eh < 0) // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找 return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { // 是链表,遍历查找 if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }

四、size() 方法执行流程

Java 8 中 size() 的返回类型是 int,但底层统计逻辑基于 long。它不是 O(1) 的操作,而是一个估算值(在并发修改时)。

1. 计数机制 (LongAdder 思想)

为了减少高并发下对单一计数变量的竞争,CHM 采用了类似 LongAdder 的机制:

  • baseCount: 基础计数值。
  • CounterCell[]: 一个数组,当并发竞争激烈时,线程会将计数累加到不同的 CounterCell 单元格中,最后求和。
2. 执行流程 (sumCount())
  1. 读取 baseCount
  2. 遍历 counterCells 数组,累加所有非空单元格的值。
  3. 总和 = baseCount + counterCells 之和。
3. 返回值
  • size() 方法内部调用 sumCount(),如果结果超过 Integer.MAX_VALUE 则返回最大值,否则强转为 int
  • 注意: 由于并发修改,size() 返回的值可能不是强一致性的(即调用瞬间的真实大小),但在大多数场景下足够准确。如果需要强一致性大小,需要加锁统计,但性能会大幅下降。

五. put 元素时如何更新计数?

下文来自ConcurrentHashMap 源码分析 | JavaGuide

六、核心优化点总结 (面试加分项)

  1. 锁粒度优化: 从 Java 7 的 Segment 分段锁(锁整个段)变为 Java 8 的 桶级别锁 (synchronized)。JVM 对 synchronized 做了大量优化(偏向锁、轻量级锁),在低冲突下性能极高。
  2. CAS 无锁插入: 如果桶为空,直接 CAS 插入,无需加锁。
  3. 红黑树优化: 当哈希冲突严重时,链表转为红黑树,查找时间复杂度从 O(n) 降为 O(log n)。
  4. 协同扩容: 扩容时,多个线程可以一起帮助迁移数据 (helpTransfer),加快了扩容速度,避免了单线程迁移导致的停顿。
  5. 读写分离: get 操作完全无锁,put 操作仅在冲突时加锁,极大提高了读多写少场景的性能。

Read more

保姆级教程!VSCode 配置 Python 环境一篇就够

保姆级教程!VSCode 配置 Python 环境一篇就够

欢迎阅读我的文章!更多精彩内容,欢迎关注: • B站主页:🫱小枫Geek • 微信公众号:Procode   • 视频教程:🫱VSCode配置Python环境   前言 刚上大学,很多同学第一次接触编程,想用 Python 入门,却常常卡在环境配置上。今天带大家从 Python 安装 → VSCode 配置 → 运行代码,一步到位搞定,不踩坑! 一、下载安装 Python 1. 打开 Python 官网。 2. 在下载页面选择 Windows 系统版本。 3. 我们只需要下载 3.0 以上版本即可,这里以 Python 3.12 为例。 下载完成后开始安装: * 其他选项默认即可,点击下一步完成安装。 重点注意:一定要勾选

By Ne0inhk
【C++写详细总结①】从for循环到算法初步

【C++写详细总结①】从for循环到算法初步

前言 本文通过小编自身学习的进程从而总结出本文,也希望大家可以好好学习,帮助到自己 这个是萌新考场救场代码,与本文一起食用更佳 for循环计数器 for(定义计数变量;定义结束条件;每次循环所做的动作) 示例 for(int i=1;i<=10;i++) //首先定义“i”变量作为计数数组,赋初值为“1”//然后每次循环判断条件是否成立,不成立则退出//最后每循环执行条件,此示例为每循环“i”增加1 而计数器就是在for循环有了一定执行范围的基础上创建了一个数组,进行++计数 示例 #include<iostream>// 万年不变的框架usingnamespace std;intmain(){int n; cin>>n;//输入数值表示从1~n中有几个数字int

By Ne0inhk
AutoGPT+Python:让AI智能体自动完成复杂任务的终极指南

AutoGPT+Python:让AI智能体自动完成复杂任务的终极指南

AutoGPT+Python:让AI智能体自动完成复杂任务的终极指南 引言:在人工智能迈向自主化的新阶段,AutoGPT作为基于大语言模型(LLM)的自主智能体代表,正掀起一场让AI自己思考、自主执行的技术革命。当它遇上Python的全栈生态与极致灵活性,开发者不再只是调用AI接口,而是能深度定制专属智能体——让AI听懂自然语言、拆解复杂目标、调用外部工具、联网检索信息、迭代优化结果,独立完成从市场调研、内容创作、代码开发到自动化运维的全流程任务。 本文从核心原理、本地部署、Python实战、插件扩展、生产优化五大维度,手把手带你从0到1搭建可落地、可监控、可进化的AI智能体系统,不管是AI爱好者、全栈开发者还是创业者,都能靠这份指南,掌握下一代人机协作的核心生产力。 一、先搞懂:AutoGPT到底是什么? 传统ChatGPT类模型是被动应答,你问一句它答一句,需要人工一步步引导;而AutoGPT是自主智能体,你只给它一个最终目标,它就能自己完成: * 任务拆解:把复杂目标拆成可执行子步骤 * 自主决策:判断下一步该做什么、调用什么工具 * 记忆管理:短期记忆存上下文

By Ne0inhk
基于SSM 高校后勤管理系统 的设计与实现--(免费领源码)可做计算机毕业设计JAVA、PHP、爬虫、APP、小程序、C# 、C++、python、大数据、全套文案

基于SSM 高校后勤管理系统 的设计与实现--(免费领源码)可做计算机毕业设计JAVA、PHP、爬虫、APP、小程序、C# 、C++、python、大数据、全套文案

SSM 高校后勤管理系统 摘 要 科技进步的飞速发展引起人们日常生活的巨大变化,电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流,人类发展的历史正进入一个新时代。在现实运用中,应用软件的工作规则和开发步骤,采用Java技术建设高校后勤管理系统。 本设计主要实现集人性化、高效率、便捷等优点于一身的高校后勤管理系统,完成用户管理、故障报修、公寓信息、食堂管理、基础设施、校园环境、校医院信息等功能模块。系统通过浏览器与服务器进行通信,实现数据的交互与变更。本系统通过科学的管理方式、便捷的服务提高了工作效率,减少了数据存储上的错误和遗漏。高校后勤管理系统使用Java语言,基于B/S架构,后端部分采用SSM 框架 进行开发,数据方面主要采用的是微软的MySQL关系型数据库来作为数据存储媒介。 关键词:高校后勤管理;SSM 框架 ;B/S架构;MySQL数据库 SSM University Logistics Management System Abstract The rapid development o

By Ne0inhk