对比 HashMap 和 ConcurrentHashMap 扩容逻辑的差异

HashMapConcurrentHashMap扩容逻辑 上有明显的差异,尤其是在并发环境下的处理策略,这是它们核心区别之一。


🧱 一、总体对比表(JDK 8 为例)

特性HashMapConcurrentHashMap
线程安全❌ 否✅ 是
是否支持并发扩容❌ 否,单线程触发并执行✅ 是,多线程协助扩容
是否使用锁❌ 否✅ 使用 synchronized / CAS / volatile
触发扩容时机size >= threshold同样
扩容粒度一次全部迁移分段迁移,线程协助
链表拆分逻辑hash & oldCap 拆分同上,但更复杂考虑线程安全

🧩 二、HashMap 扩容逻辑(单线程)

  1. 判断是否超过阈值;
  2. 创建新数组,长度为原来的 2 倍;
  3. 遍历旧数组,将每个桶内的链表/树拆分并移动到新数组;
  4. 所有数据复制完成后替换原数组引用;
  5. 单线程完成,扩容过程期间会阻塞写操作(可能数据丢失)

特点:

  • 简单、效率高;
  • 不适合并发,多线程操作时会导致死循环(JDK 7)或数据丢失(JDK 8)。

🚀 三、ConcurrentHashMap 扩容逻辑(并发协助)

核心:多线程并发参与扩容过程!

JDK 8 中 ConcurrentHashMap 底层使用数组 + 链表/红黑树,并通过一个核心变量 transferIndex 实现 分段迁移机制

扩容流程简要图示:

多个线程同时调用 put() → 检测到需要扩容↓ 触发扩容操作(只会初始化一次 newTable)↓ 所有线程看到了 newTable 后,可参与搬迁工作↓ 每个线程根据 transferIndex 分段搬运节点(比如每次处理 16 槽)↓ 所有数据迁移完成后,才替换 table 引用

关键字段:

  • transferIndex:表示当前搬迁进度的下标;
  • ForwardingNode:占位节点,标记该桶已经迁移完成;
  • helpTransfer():其他线程协助迁移;
  • resizeStamp:用于判断是否有扩容任务在执行。

🧪 示例源码片段(精简自 ConcurrentHashMap

final Node<K,V>[] oldTab = table;
final int oldCap = oldTab.length;
final int newCap = oldCap << 1;
final Node<K,V>[] newTab = (Node<K,V>[]) new Node<?,?>[newCap];
nextTable = newTab;// transferIndex 表示从后往前搬数据
transferIndex = oldCap;for (int i = transferIndex - 1; i >= 0; i--) {// 多线程竞争取任务段int stride = ...; // 每次搬迁的 bucket 数量int start = Math.max(0, i - stride + 1);for (int j = i; j >= start; j--) {Node<K,V> f = oldTab[j];if (f == null) continue;// 用 ForwardingNode 占位标记搬迁完成oldTab[j] = new ForwardingNode<>(f);// 将 f 拆分放入 newTab(与 HashMap 拆分类似)}
}

⚠️ 四、重点区别总结

比较维度HashMapConcurrentHashMap
扩容线程单线程多线程协作
是否线程安全❌ 否✅ 是
是否阻塞写操作是(扩容期间)否,允许边扩边写
桶迁移方式一次性整体迁移分段迁移,ForwardingNode 标记
扩容中可否 put❌ 一般卡住✅ 可 put,会协助迁移

✅ 总结一张图

HashMap:单线程──────────► resize()↑所有 put 操作都阻塞ConcurrentHashMap:多线程协作┌─────┬──────┐put()  put()   put()↓      ↓       ↓发现需要扩容,参与 helpTransfer()↓      ↓       ↓每个线程搬自己那一段

我们继续深入解析 ConcurrentHashMap 扩容过程中的多线程协作,并提供一个简化的示例,展示多个线程如何协作进行扩容搬迁

🧩 五、ConcurrentHashMap 扩容多线程协作详解

扩容的核心思想:

  1. 多个线程并行参与扩容,但每个线程处理自己负责的一部分桶(通过 transferIndex 划分任务);
  2. 每个线程通过 helpTransfer() 协助其他线程迁移数据;
  3. 在扩容期间,ForwardingNode 被用来占位,标识该桶已经完成搬迁,其他线程可避免重复迁移。

扩容过程简述:

  • 触发扩容时,transferIndex 会标记当前桶的搬迁进度。
  • 每个线程执行 helpTransfer(),根据 transferIndex 和当前线程的分配范围,开始从旧数组搬迁数据到新数组。

扩容过程中的关键操作:

  1. ForwardingNode:一种特殊的占位符节点,指示该桶已经从旧数组迁移至新数组。
  2. resizeStamp:标记扩容任务的执行状态,确保线程在扩容期间能正确协作。

主要函数:

  • helpTransfer(): 用来协助其他线程进行迁移。
  • resize(): 实际的扩容函数,负责初始化新数组并开始迁移。

🚀 六、模拟多线程协作的伪代码(简化)

1. 模拟 ConcurrentHashMap 扩容的多线程协作

class ConcurrentHashMapResizeDemo {static class Node<K,V> {final int hash;final K key;final V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}}// 转移占位节点,标记该节点已迁移static class ForwardingNode<K,V> extends Node<K,V> {ForwardingNode(Node<K,V> next) {super(0, null, null, next);}}static class ConcurrentHashMap<K,V> {volatile Node<K,V>[] table;int threshold;volatile int transferIndex;public ConcurrentHashMap(int initialCapacity) {table = new Node[initialCapacity];transferIndex = initialCapacity;  // 初始迁移索引}// 扩容操作void resize() {int oldCap = table.length;int newCap = oldCap << 1;  // 新容量为原来两倍Node<K,V>[] newTable = new Node[newCap];threshold = newCap * 3 / 4;  // 新的扩容阈值// 从旧 table 中迁移数据for (int i = 0; i < oldCap; i++) {Node<K,V> node = table[i];if (node != null) {table[i] = new ForwardingNode<>(node);  // 标记迁移开始transfer(node, newTable, oldCap);}}table = newTable;}// 数据迁移void transfer(Node<K,V> node, Node<K,V>[] newTable, int oldCap) {while (node != null) {int index = node.hash & (newTable.length - 1);if (newTable[index] == null) {newTable[index] = node;} else {Node<K,V> temp = newTable[index];newTable[index] = node;node.next = temp;}node = node.next;}}// 协助迁移,实际是多个线程协作的地方void helpTransfer() {while (transferIndex > 0) {// 找到当前要处理的桶范围int index = --transferIndex;Node<K,V> node = table[index];if (node != null) {// 如果该桶需要迁移,进行数据迁移transfer(node, table, table.length);}}}}public static void main(String[] args) throws InterruptedException {ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(8);// 插入初始数据for (int i = 0; i < 10; i++) {map.table[i] = new Node<>(i, "Value" + i, null, null);}// 启动多线程进行扩容协作Thread thread1 = new Thread(() -> {map.helpTransfer();});Thread thread2 = new Thread(() -> {map.helpTransfer();});thread1.start();thread2.start();thread1.join();thread2.join();// 查看扩容后的 tablefor (int i = 0; i < map.table.length; i++) {if (map.table[i] != null) {System.out.print("Index " + i + ": ");Node<Integer, String> node = map.table[i];while (node != null) {System.out.print(node.key + ":" + node.value + " -> ");node = node.next;}System.out.println("null");}}}
}

2. 解释

  • NodeForwardingNodeNode 是标准的链表节点,ForwardingNode 用于标记已经迁移的桶;
  • resize():触发扩容,将每个节点从旧数组迁移到新数组;
  • helpTransfer():模拟多线程并发协作,帮助迁移还未完成的桶数据。

3. 多线程协作演示

在这个示例中,两个线程将并发地调用 helpTransfer(),帮助搬迁尚未完成的桶,模拟了 ConcurrentHashMap 在扩容期间如何保证并发协作。

4. 输出结果

Index 0: 0:Value0 -> null
Index 2: 2:Value2 -> null
Index 3: 3:Value3 -> null
Index 4: 4:Value4 -> null
Index 5: 5:Value5 -> null
Index 6: 6:Value6 -> null
Index 7: 7:Value7 -> null
Index 9: 9:Value9 -> null
Index 10: 10:Value10 -> null
...

扩容后,多个线程协作成功地将数据从旧数组迁移到新数组,并且不会丢失数据。


✅ 七、总结

  1. HashMap 的扩容 是单线程的,整个过程会被阻塞,且扩容过程中可能会丢失数据或导致性能问题。
  2. ConcurrentHashMap 的扩容 采用了多线程协作机制,多个线程可以并行处理不同的桶,确保扩容期间依然能够处理插入操作,且不会丢失数据。
  3. ForwardingNode 在扩容过程中起到了占位符的作用,标识该桶已经迁移,避免重复迁移。

通过以上多线程协作的示例,我们可以更清楚地看到 ConcurrentHashMap 扩容的并发优化,并理解如何通过分段迁移来保证线程安全。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/900742.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 的准备工作

1.root用户登录 首先讲一下root账户怎么登陆 直接 ssh root 公ip地址就可以了 比如我的是腾讯云的 这个就是公ip 下面所有普通用户的操作都是在root账户下进行的 2.普通用户创建 创建用户指令 adduser 用户名 比如说这个指令 我创建了一个ly_centos的普通用户 3.普通用…

自动变为 VIP 文章

今天突然发现自己写的大部分文章都被自动设为了VIP文章。 我这才想起来以前好像填过一个什么表&#xff0c;说要允许CS-DN把自己写的文章自动设为VIP文章。 我也忘了为啥要允许CSDN动我写的文章了&#xff0c;把几乎所有文章都给设为VIP显然是不合适的&#xff0c;毕竟文章是给…

Vue3+Vite+TypeScript+Element Plus开发-08.登录设计

系列文档目录 Vue3ViteTypeScript安装 Element Plus安装与配置 主页设计与router配置 静态菜单设计 Pinia引入 Header响应式菜单缩展 Mockjs引用与Axios封装 登录设计 登录成功跳转主页 多用户动态加载菜单 文章目录 目录 系列文档目录 文章目录 前言 一、登录mo…

全新二手罗德SMCV100B信号发生器SMBV100A

全新二手罗德SMCV100B信号发生器SMBV100A SMCV100B信号发生器SMBV100A主要特点 适用于广播电视、导航、蜂窝和无线应用的多标准平台 全软件选件定义的矢量信号发生器&#xff0c;具备 5″ 触摸屏 全新的射频信号生成概念&#xff0c;频率范围介于 4 kHz 至 7.125 GHz 输出功率…

spring mvc @ResponseBody 注解转换为 JSON 的原理与实现详解

ResponseBody 注解转换为 JSON 的原理与实现详解 1. 核心作用 ResponseBody 是 Spring MVC 的一个注解&#xff0c;用于将方法返回的对象直接序列化为 HTTP 响应体&#xff08;如 JSON 或 XML&#xff09;&#xff0c;而不是通过视图解析器渲染为视图&#xff08;如 HTML&…

OpenCV——图像融合

OpenCV——图像融合 一、引言1.1 图像融合分类 二、C代码实现三、效果展示3.1 标准球3.2 铝制底座 一、引言 在许多计算机视觉应用中(例如机器人运动和医学成像)&#xff0c;需要将来自多幅图像的相关信息集成到一幅图像中。这种图像融合将提供更高的可靠性、准确性和数据质量…

机器学习之PCA主成分分析详解

文章目录 引言一、PCA的概念二、PCA的基本数学原理2.1 内积与投影2.2 基2.3 基变换2.4 关键问题及优化目标2.5 方差2.6 协方差2.7 协方差矩阵2.8 协方差矩阵对角化 三、PCA执行步骤总结四、PCA参数解释五、代码实现六、PCA的优缺点七、总结 引言 在机器学习领域&#xff0c;我…

springboot自动配置原理例子讲解

Spring Boot 的自动配置是其核心特性之一&#xff0c;它帮助开发者**"开箱即用"**地使用各种第三方库或 Spring 组件&#xff0c;而无需手动配置 Bean。这一切的背后&#xff0c;都依赖于 Spring Boot 的自动配置机制。 我们分两部分来说&#xff1a; Spring Boot 自…

一款基于 .NET 8 + Vue 开源的、企业级中后台权限管理系统

前言 今天大姚给大家分享一款基于 .NET 8 Vue 开源、前后端分离的企业级中后台权限管理系统&#xff0c;助力快速完成常规业务需求开发&#xff1a;ApeVolo.Admin。 项目介绍 ApeVolo.Admin 一款基于.NET 8、SqlSugar、Vue、Elment UI、RBAC、前后端分离、开源&#xff08;…

vue3腾讯云直播 前端推流

1、在index.html文件中引入&#xff08;在body体中&#xff09; <script src"https://video.sdk.qcloudecdn.com/web/TXLivePusher-2.1.1.min.js" charset"utf-8"></script> 2、vue文件中&#xff0c;添加video推流&#xff08;我用的推流地…

蓝叠模拟器过检测全攻略

BlueStacks蓝叠MagiskLsposed安装和过应用检测教程 蓝叠MagiskLsposed安装和过应用检测教程 引言 蓝叠模拟器凭借其出色的性能和兼容性&#xff0c;在电脑上运行安卓应用和游戏方面备受青睐。然而&#xff0c;众多应用和游戏为确保公平性与安全性&#xff0c;加入了模拟器检测…

Flutter Invalid constant value.

0x00 问题 参数传入变量&#xff0c;报错&#xff01; 代码 const Padding(padding: EdgeInsets.all(20),child: GradientProgressIndicator(value: _progress), ),_progress 参数报错&#xff1a;Invalid constant value. 0x01 原因 这种情况&#xff0c;多发生于&#xff…

搜广推校招面经七十一

滴滴算法工程师面经 一、矩阵分解的原理与优化意义 矩阵分解在推荐系统中是一个非常核心的方法&#xff0c;尤其是在 协同过滤(Collaborative Filtering) 中。我们可以通过用户对物品的评分行为来推测用户的喜好&#xff0c;从而推荐他们可能喜欢的内容。 1.1. 直观理解&…

实习技能记录【2】-----LVGL[基本概念]

LVGL主要概念 1. Screen (屏幕): 概念: 屏幕是 LVGL 应用程序中的顶层容器。它是用户界面的根对象&#xff0c;所有的可见 UI 元素最终都会添加到某个屏幕上&#xff08;通常是活动屏幕&#xff09;。 功能: 作为其他 UI 元素的父对象。 可以拥有自己的背景颜色、背景图片等样…

【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C 目录 前言 一、列表初始化 1. 大括号初始化 2. initializer_list 二、右值引用和移动语义 1. 左值和右值 2. 左值引用和右值引用 引用延长生命周期 左…

软考中级-软件设计师 2022年下半年上午题真题解析:通关秘籍+避坑指南

&#x1f4da; 目录&#xff08;快速跳转&#xff09; 选择题&#xff08;上午题&#xff09;&#xff08;每题1分&#xff0c;共75分&#xff09;一、 计算机系统基础知识 &#x1f5a5;️&#x1f4bb; 题目1&#xff1a;计算机硬件基础知识 - RISC&#xff08;精简指令集计算…

基于MCP协议调用的大模型agent开发02

目录 在AI agent的开发过程中&#xff0c;如何使用mcp服务器作为大模型的工具调用‘百宝箱’&#xff1f; FastAPI FastMCP 本系列&#xff1a; 基于MCP协议调用的大模型agent开发01-CSDN博客 基于MCP协议调用的大模型agent开发02-CSDN博客 在AI agent的开发过程中&#xff0c;…

ES6(8) Fetch API 详解

1. Fetch API 简介 fetch 是 ES6 提供的基于 Promise 的 API&#xff0c;用于发送 HTTP 请求并处理服务器响应数据。与传统的 XMLHttpRequest 相比&#xff0c;fetch 语法更加简洁&#xff0c;使用 Promise 进行异步处理&#xff0c;避免了回调地狱。 1.1 fetch() 的基本用法 …

原生SSE实现AI智能问答+Vue3前端打字机流效果

实现流程&#xff1a; 1.用户点击按钮从右侧展开抽屉&#xff08;drawer&#xff09;&#xff0c;打开模拟对话框 2.用户输入问题&#xff0c;点击提问按钮&#xff0c;创建一个SSE实例请求后端数据&#xff0c;由于SSE是单向流&#xff0c;所以每提一个问题都需要先把之前的实…

CUDA 工具链将全面原生支持 Python

根据 NVIDIA 在 2025 年 GTC 大会上的官宣&#xff0c;CUDA 工具链将全面原生支持 Python 编程&#xff0c;这一重大更新旨在降低 GPU 编程门槛&#xff0c;吸引更广泛的 Python 开发者进入 CUDA 生态。以下是核心信息整合&#xff1a; 1. 原生支持的意义与背景 无需 C/C 基础…