深入剖析ConcurrentHashMap:高并发场景下的线程安全与性能优化
1. 从一次线上死锁说起为什么我们需要ConcurrentHashMap那天晚上系统监控突然报警一个核心服务的接口响应时间飙升紧接着线程池被打满整个服务几乎陷入停滞。紧急排查日志发现大量线程卡在同一个地方——一个看似普通的HashMap.get()操作上。是的在一个高并发的场景下我们团队里一位同事图省事用了一个未加锁的HashMap来缓存一些全局配置信息。当多个线程同时执行put和get时触发了HashMap在扩容时的死循环问题直接导致了这次线上事故。这次惨痛的教训让我们所有人都深刻意识到在并发编程的世界里容器的选择绝不是小事。而ConcurrentHashMap正是为应对这种高并发、线程安全的键值对存取场景而生的利器。它不是简单的HashMap加锁而是一套从设计哲学到实现细节都充满巧思的并发艺术品。今天我们就抛开那些泛泛而谈的面试八股深入到代码层面把ConcurrentHashMap以下简称 CHM的“五脏六腑”拆开来看看看它到底是如何在保证线程安全的同时还能保持如此高的性能。对于 Java 开发者而言无论你是刚入门的新手还是有一定经验的工程师理解 CHM 都至关重要。新手可以借此建立起对 Java 并发包 (java.util.concurrent) 优秀设计的第一印象而有经验的开发者则能通过理解其内部机制在诸如缓存设计、计数器、会话存储等实际场景中做出更合理的选择避免踩坑。网络上关于concurrenthashmap computifabsent bug的讨论恰恰说明了即使是这样成熟的工具如果对其原理理解不透彻也可能会遇到意想不到的问题。接下来我们就从最核心的设计思想开始。2. 摒弃全局锁CHM 的并发设计基石与核心思想在早期JDK 1.5之前如果想得到一个线程安全的 Map通常有两种选择一是使用Collections.synchronizedMap(new HashMap())包装一个HashMap二是直接使用Hashtable。这两种方式的本质都是在每个方法上加了synchronized关键字即对整个 Map 对象进行全局锁。想象一下一个超市只有一个收银台无论顾客是买一瓶水还是推一车货都必须排队。这在顾客线程很少的时候没问题但一旦人多了效率就会急剧下降。这就是全局锁的弊端它粗暴地让所有操作串行化严重限制了并发吞吐量。ConcurrentHashMap的设计哲学就是打破这种“一个收银台”的模型。它的核心思想是“锁分离”和“锁细化”。它不再对整个数组加锁而是将数据分成一段一段在 JDK 1.7 及之前称为 Segment在 JDK 1.8 及之后演变为对每个数组桶的头节点加锁每次操作只锁住其中一小部分。这样当线程 A 在操作数组索引为 1 的桶时线程 B 完全可以同时操作索引为 15 的桶两者互不干扰并发度得到了质的提升。这个思想演进到 JDK 1.8实现变得更加精巧和统一。JDK 1.8 的 CHM 摒弃了略显笨重的Segment分段锁机制回归到与HashMap更相似的结构数组 链表 红黑树。它的锁粒度进一步细化到了每个数组桶bucket的头节点。只有在对同一个桶进行写操作如 put, remove时才会使用synchronized锁住这个桶的头节点。对于读操作get则完全不加锁依赖于volatile关键字和Unsafe类提供的原子操作来保证可见性。这种设计使得在绝大多数以读为主或读写冲突不严重的场景下性能几乎与无锁的HashMap无异。那么它是如何做到读操作不加锁的呢关键在于几个特殊的变量和内存操作。Node节点中的val和next字段都被声明为volatile。这意味着当一个线程修改了某个节点的值或链表结构后这个修改会立即被写回主内存并且使其他线程中对应的缓存行失效从而保证其他线程能读到最新的值。对于数组引用table本身它的扩容等操作也通过volatile和Unsafe.compareAndSwap等原子操作来保证线程安全。所以一个get操作本质上就是一次无锁的链表或树的遍历速度极快。注意这里说的“读操作不加锁”指的是get方法本身。在迭代器遍历、计算size()等需要一致性视图的操作中CHM 有更复杂的处理逻辑可能会遇到弱一致性的情况这并非 bug而是其在高性能和高并发之间做出的设计权衡。3. 庖丁解牛JDK 1.8 ConcurrentHashMap 核心数据结构与源码探秘要真正理解 CHM我们必须深入到它的数据结构里去看。在 JDK 1.8 中CHM 的内部存储结构与HashMap高度相似但每个部分都为了并发进行了强化。3.1 Node 节点一切的基础首先是最基础的存储单元NodeK,V。它和HashMap的节点很像但有至关重要的不同。static class NodeK,V implements Map.EntryK,V { final int hash; final K key; volatile V val; // 值被 volatile 修饰 volatile NodeK,V next; // 下一个节点引用也被 volatile 修饰 // ... 构造方法和其他方法 }注意val和next都被声明为volatile。这就是读操作无需加锁的底气所在。volatile保证了多线程环境下变量的可见性和有序性禁止指令重排序。当一个线程修改了节点的值另一个线程能立刻看到这个变化。final修饰的hash和key保证了节点在链表中的不可变性这是实现安全并发的重要前提。3.2 核心字段解析打开ConcurrentHashMap的源码我们会看到几个关键的字段transient volatile NodeK,V[] table;这就是主哈希表数组。它本身被volatile修饰保证了数组引用的可见性。但请注意volatile只保证table这个引用本身的可见性不保证数组内部元素的可见性。数组元素的可见性由每个Node节点的volatile字段来保证。private transient volatile NodeK,V[] nextTable;扩容时使用的新数组。同样用volatile修饰。private transient volatile int sizeCtl;这是一个非常关键的控制变量它是一个“多功能字段”。它的不同取值代表不同状态-1表示哈希表正在初始化。-(1 n)表示有 n 个线程正在协助扩容。0 或正数如果表未初始化则表示初始容量或默认容量0如果表已初始化则表示下一次扩容的阈值容量 * 负载因子。这个字段通过CASCompare-And-Swap操作来更新是 CHM 实现无锁化控制的核心。3.3 putVal 流程并发插入的精密舞蹈put方法是 CHM 并发控制的集大成者。我们跟随着putVal方法的源码来看一次插入操作是如何在并发环境下安全完成的。计算哈希与定位桶首先计算 key 的哈希值。CHM 使用了(h ^ (h 16)) HASH_BITS的方式来扰动哈希并屏蔽掉负的哈希值最高位为0这是为了特殊节点如ForwardingNode预留标识。然后通过(n - 1) hash定位到数组下标i。桶为空时的无锁插入如果table[i]为null说明这个桶还没有元素。这时CHM 不会直接加锁而是尝试使用CAS操作U.compareAndSwapObject将新创建的节点设置到数组的这个位置。如果CAS成功插入就完成了速度极快。如果CAS失败说明就在这一瞬间有其他线程抢先在这个位置插入了节点那么当前线程就需要进入后续的加锁流程。这一步是提升并发性能的关键优化。桶非空时的加锁处理如果桶不为空或者上一步CAS失败则需要对该桶的头节点进行加锁synchronized (f)其中f是当前桶的头节点。链表插入如果f.hash 0说明这是一个普通的链表节点。这时会遍历链表如果找到相同的 key 则更新 value否则将新节点插入链表尾部JDK 1.8是尾插法避免了1.7之前扩容时的死链问题。红黑树插入如果f是TreeBin类型哈希值为TREEBIN即 -2说明这个桶已经树化了。那么会调用红黑树的putTreeVal方法进行插入。TreeBin内部也维护了自己的读写锁来保证并发修改下红黑树的平衡性。遇到 ForwardingNode如果f是ForwardingNode哈希值为MOVED即 -1说明当前哈希表正在扩容。那么当前线程不会阻塞而是会调用helpTransfer方法去协助扩容扩容完成后再在新的数组上执行插入操作。这是 CHM 支持并发扩容的关键。计数与扩容检查插入成功后会调用addCount方法。这个方法做了两件重要的事更新元素数量CHM 没有像HashMap那样用一个简单的size变量而是使用了一个CounterCell数组一种分片计数器的思想类似于LongAdder来统计元素个数避免更新size成为热点竞争。检查是否需要扩容在addCount中会检查元素总数是否超过阈值sizeCtl。如果超过则触发transfer方法进行扩容。扩容本身也是一个可以多线程并发协助的过程。整个put流程体现了 CHM “能无锁则无锁需加锁则细锁”的设计原则并且将扩容这个重型操作也设计成了并发可协助的极大提升了高并发下的吞吐量。4. 扩容机制多线程协同的数据迁移艺术HashMap 的扩容Rehashing是一个比较耗时的操作需要创建新数组并遍历旧数组的所有元素重新计算哈希值并放入新数组。在并发环境下如果让一个线程独自完成这个工作其他线程都会被阻塞这是不可接受的。CHM 的扩容机制transfer()是其并发设计的精华所在。4.1 触发扩容的时机扩容主要由addCount方法触发。当插入元素后检测到总元素数超过阈值sizeCtl时会调用transfer()。第一个发起扩容的线程会将sizeCtl设置为一个负值表示扩容开始并计算出新数组nextTable。4.2 如何实现并发扩容—— 任务分片CHM 将整个旧数组划分成多个小的“任务块”。它维护一个transferIndex变量初始值等于旧数组长度n。每个参与扩容的线程会通过CAS操作去竞争一个任务块。竞争的方式是CAS将transferIndex减去一个步长stride最小为16成功者就获得了从transferIndex到transferIndex-stride这个区间的迁移任务。这就像一个大工程被分成了很多小包工人们线程可以主动去领包干活干完再领直到所有包被领完。4.3 迁移过程中的读写协同这是最精妙的部分扩容期间旧的table和新创建的nextTable同时存在。如何保证在此期间get、put等操作还能正确进行答案是ForwardingNode转发节点。当一个桶比如下标为i的桶的数据被某个线程迁移完成后它会在旧数组的这个位置放置一个ForwardingNode节点。这个节点的hash值为MOVED-1并且它持有新数组nextTable的引用。对于读操作get当线程遍历到某个桶发现头节点是ForwardingNode时它就知道这个桶的数据已经迁移到新数组了。于是它会调用ForwardingNode的find方法该方法会引导线程到新数组的对应位置去查找数据。因此读操作在扩容期间也能正常进行几乎不受影响。对于写操作put当线程准备向某个桶插入数据发现头节点是ForwardingNode时它不会阻塞等待扩容完成而是会主动调用helpTransfer()方法参与到扩容工作中去。等协助完成这个桶或一部分区域的迁移后再继续执行自己的插入操作。这就是“协助扩容”机制它巧妙地将写线程从阻塞者变成了协作者加快了整体扩容速度。4.4 扩容完成当所有任务块都被领取并迁移完成后最后一个完成迁移的线程会执行收尾工作将table引用指向新的nextTable并重新计算sizeCtl为新的扩容阈值新容量的 0.75 倍。至此扩容完成所有线程后续的操作都将基于新的、更大的数组进行。这种设计使得 CHM 的扩容过程不再是性能瓶颈反而可以利用多线程资源加速完成充分体现了其“化阻塞为协作”的高明之处。5. 从 computeIfAbsent 的“Bug”谈CHM的线程安全边界与适用场景网络上讨论的concurrenthashmap computifabsent bug其实是一个经典的误解它恰恰揭示了 CHM 线程安全的一个关键边界。我们先看这个方法的定义V computeIfAbsent(K key, Function? super K, ? extends V mappingFunction)。它的作用是如果 key 不存在则使用mappingFunction计算一个 value 并放入 Map如果 key 存在则直接返回已有的 value。问题出在mappingFunction上。CHM 保证的是Map内部数据结构数组、链表、树在并发修改下的线程安全但它不保证传入的mappingFunction只被执行一次。来看一段可能出问题的代码ConcurrentHashMapString, AtomicInteger map new ConcurrentHashMap(); map.computeIfAbsent(key, k - { // 这个函数体可能会被多个线程执行 System.out.println(Computing value for k); return new AtomicInteger(0); }).incrementAndGet();在极高并发下两个线程可能同时发现“key”不存在它们都会进入computeIfAbsent的逻辑并且都可能执行mappingFunction。虽然最终只有一个线程计算出的AtomicInteger对象会被成功放入 Map得益于putVal内部的锁或 CAS但mappingFunction本身这里包括new AtomicInteger(0)却被执行了多次。如果这个函数体非常耗时比如查询数据库或者有副作用比如上面的打印语句就会产生非预期的结果。这根本不是 Bug而是 CHM 在性能与严格一致性之间做出的权衡。如果要保证函数绝对只执行一次就需要在桶级别甚至整个 Map 级别进行更严格的同步这会严重损害性能。CHM 的选择是保证最终放入 Map 的结果是正确的只有一个值但不保证计算过程的无重入。提示如果你需要确保mappingFunction只执行一次有几种方案1使用putIfAbsent结合双重检查但逻辑稍复杂2使用ConcurrentHashMap的computeIfAbsent但确保你的mappingFunction是幂等的多次执行结果相同且无副作用3考虑使用 Guava 的CacheLoader或自己实现一个带原子引用的单次计算逻辑。这个案例引出了 CHM 的适用场景。它最适合的是“读多写少”或“写操作冲突不频繁”的高并发缓存场景。例如全局配置缓存系统启动时加载运行期间极少修改但所有线程频繁读取。Session 存储用户会话信息以 Session ID 为 key读写比例相对均衡但 CHM 的细粒度锁能很好地处理。计数器统计结合AtomicLong或LongAdder作为 value使用computeIfAbsent和increment进行分维度统计。而不太适合的场景是需要强一致性的迭代CHM 的迭代器是“弱一致性”的它反映的是迭代器创建时或创建后的某个瞬间的状态不保证能遍历到迭代过程中被其他线程修改的内容。写操作极度频繁且键高度集中如果所有线程都在疯狂地对同一个键进行put那么对这个桶的锁竞争会非常激烈性能会退化成类似Hashtable。这时可能需要考虑分段或使用其他数据结构。理解这些边界能帮助我们在正确的场景下使用这把“瑞士军刀”避免将其用作“万能锤子”。

相关新闻

动画单集对比分析框架:从画面到叙事的系统评测方法

动画单集对比分析框架:从画面到叙事的系统评测方法

这次我们来看一个针对动画单集质量进行对比分析的项目。它不是一个软件工具或AI模型,而是一个聚焦于动画剧集制作水准、画面表现、叙事节奏等维度的深度评测与对比框架。对于动画爱好者、从业者或内容创作者而言,它提供了一套系统化的方法来拆解和评判一…

2026/10/26 16:29:03 阅读更多 →
商品状态管理实战:从数据库设计到前端展示的“已失效”状态处理方案

商品状态管理实战:从数据库设计到前端展示的“已失效”状态处理方案

在实际的电商或内容平台开发中,我们经常会遇到商品或内容状态管理的需求。例如,一个商品可能处于“上架”、“下架”、“售罄”或“已失效”等不同状态。当用户在前端看到“已失效”的标签时,背后是一套完整的状态流转逻辑和数据过滤机制在支…

2026/10/24 12:41:25 阅读更多 →
4GB显存微调8B大模型:Soup工具包实战与QLoRA技术解析

4GB显存微调8B大模型:Soup工具包实战与QLoRA技术解析

这次我们来看一个名为 Soup 的开源项目,它最新的 v0.72.4 版本主打一个核心卖点:在仅有 4GB 显存的消费级笔记本 GPU 上,也能完成对 8B(80亿)参数级别大语言模型的微调。对于很多想尝试模型定制化但苦于硬件门槛的研究…

2026/10/25 15:21:45 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/10/26 12:00:21 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/10/25 22:36:10 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/10/26 4:52:12 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/10/26 12:00:21 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/10/26 5:00:37 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/10/26 3:16:46 阅读更多 →

日新闻

周新闻

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 12:00:19 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/25 13:21:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/26 8:34:37 阅读更多 →