一个 computeIfAbsent 嵌套调用,把单核打满并且永不退出:ConcurrentHashMap 的 4 个致命细节
title: 一个 computeIfAbsent 嵌套调用把单核打满并且永不退出ConcurrentHashMap 的 4 个致命细节tags: [Java, 并发编程, ConcurrentHashMap, JDK源码, CAS]category: Java 后端一个永远跑不完的定时任务我们有个配置解析服务每 5 分钟拉一次规则中心的全量配置解析成内存对象。JDK 8u202服务跑了两年没出过事。某个周四早上监控告警说这个服务的 CPU 从常态 8% 涨到 27% 并且不再下来。看单核有一个核心持续 100%。奇怪的是接口正常响应、内存平稳、GC 正常、日志里那个定时任务的开始解析打印出来了解析完成一直没打。jstack抓了三次同一个线程一直卡在同一个地方config-refresh-1 #47 daemon prio5 os_prio0 tid0x00007f8c... nid0x2f13 runnable [0x00007f8b...] java.lang.Thread.State: RUNNABLE at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660) at com.xxx.config.RuleResolver.resolve(RuleResolver.java:52) at com.xxx.config.RuleResolver.lambda$resolve$0(RuleResolver.java:55) at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660) at com.xxx.config.RuleResolver.resolve(RuleResolver.java:52)注意状态是RUNNABLE不是BLOCKED——它在忙等占着 CPU 空转。而且栈里computeIfAbsent出现了两次中间夹着我们自己的resolve。那段递归解析的代码规则之间有继承关系规则 B 可以extends规则 A解析 B 的时候要先把 A 解析出来。当时的写法是拿ConcurrentHashMap做解析结果的缓存递归解析父规则public class RuleResolver { private final ConcurrentHashMapString, Rule cache new ConcurrentHashMap(); public Rule resolve(String ruleId) { return cache.computeIfAbsent(ruleId, id - { RawRule raw ruleRepository.load(id); Rule parent null; if (raw.getParentId() ! null) { parent resolve(raw.getParentId()); // 递归又回到 computeIfAbsent } return Rule.build(raw, parent); }); } }这段代码在 JDK 8 上跑了两年一直相安无事。周四出事的原因是运营那天新配了一条规则promo_v2它的parentId是promo_base而promo_base和promo_v2这两个 key 恰好哈希到了同一个桶。同桶递归 死循环。为什么同桶递归会死循环先看computeIfAbsent的源码骨架JDK 8u202ConcurrentHashMap.java1660 行附近public V computeIfAbsent(K key, Function? super K, ? extends V mappingFunction) { if (key null || mappingFunction null) throw new NullPointerException(); int h spread(key.hashCode()); V val null; int binCount 0; for (NodeK,V[] tab table;;) { // 关键这是个无限 for NodeK,V f; int n, i, fh; if (tab null || (n tab.length) 0) tab initTable(); else if ((f tabAt(tab, i (n - 1) h)) null) { // 桶是空的用 CAS 直接放一个占位节点不加锁 NodeK,V r new ReservationNodeK,V(); synchronized (r) { if (casTabAt(tab, i, null, r)) { binCount 1; NodeK,V node null; try { if ((val mappingFunction.apply(key)) ! null) node new NodeK,V(h, key, val, null); } finally { setTabAt(tab, i, node); // 计算完才把真节点写回 } } } if (binCount ! 0) break; } else if ((fh f.hash) MOVED) tab helpTransfer(tab, f); // 正在扩容帮忙搬运 else { boolean added false; synchronized (f) { // 桶不空锁住桶首节点 if (tabAt(tab, i) f) { // ... 遍历链表/红黑树没找到就调 mappingFunction 计算并插入 } } if (binCount ! 0) break; } } if (val ! null) addCount(1L, binCount); return val; }四个细节逐个说细节一空桶用ReservationNode占位。第一次解析promo_v2时它的桶是空的走到第 12 行。CHM 会 new 一个ReservationNode哈希值为RESERVED -3synchronized (r)锁住它CAS 塞进桶里然后才调用mappingFunction.apply(key)。也就是说计算函数是在持有桶锁的情况下执行的而此时桶里躺着一个哈希值为 -3 的占位节点。细节二递归进来时找不到匹配也不满足任何插入分支。递归调用resolve(promo_base)时因为两个 key 同桶tabAt(tab, i)拿到的不是 null而是那个ReservationNode。于是走到第 30 行的else分支synchronized (f)——这里f就是ReservationNode而当前线程已经持有它的锁synchronized 可重入所以不会阻塞能进去。进去之后遍历ReservationNode的 hash 是 -3既不等于promo_base的 hash也不是链表节点fh 0不成立也不是TreeBinf instanceof TreeBin不成立。结果就是什么分支都没命中binCount还是 0。细节三binCount 0就不 break回到for (;;)重头再来。看第 27 行和第 35 行只有binCount ! 0才跳出循环。既然什么都没做binCount恒为 0这个 for 循环就永远转下去。线程状态是RUNNABLE单核跑满 100%永不退出。细节四这不是死锁是活锁。死锁至少jstack会检测出来并打印 Found one Java-level deadlock。活锁不会——它看起来像在正常工作只是永远做不完。这也是为什么我们的监控没有任何一条规则命中CPU 高不到告警阈值27% 总体线程数正常没有异常日志。这个问题是 JDK 官方 bug JDK-8062841在 JDK 9 中被修复。修复方式不是让它能正常工作而是让它快速失败// JDK 9 的 computeIfAbsentelse 分支里多了这么一段 else if (f instanceof ReservationNode) throw new IllegalStateException(Recursive update);我后来在 JDK 11.0.16 上跑同样的代码立刻抛IllegalStateException: Recursive update栈很清楚两分钟就能定位。这也是我一直主张新项目至少从 JDK 11 起步的一个具体理由——不是为了新语法是为了这类把隐性死循环变成显性异常的修复。我们最开始查的是 GC 和死锁回顾排查过程有两个小时是浪费掉的。第一个错误方向以为是 GC。单核 100%第一反应是某个 GC 线程在空转。看 GC 日志Young GC 每 40 秒一次、耗时 12msFull GC 一次都没有。jstat -gcutil也正常。排除掉花了 25 分钟。第二个错误方向找死锁。jstack输出的末尾没有 deadlock 段落但我们还是手工比对了所有BLOCKED线程的锁持有关系确认没有环。这一步花了 40 分钟纯属浪费——因为它压根不是死锁。真正有用的一步连续抓三次 jstack比对同一个线程的栈。三次的栈完全一致且是RUNNABLE状态卡在 JDK 内部方法上。这个组合几乎只有一种可能JDK 内部的循环没能退出。搜computeIfAbsentRUNNABLE无限循环第一条就是那个 JDK bug。经验固化RUNNABLE状态 栈不动 CPU 单核满 找活锁不要找死锁。死锁看BLOCKED活锁看RUNNABLE。这两个的排查路径完全不同。四种改法方案是否根治递归问题线程安全性能适用场景A. 升级到 JDK 9否变成抛异常问题仍在是不变只能算让问题可见B. 递归改迭代先展平继承链再填 map是是最好我们的选择C. 换成Collections.synchronizedMap是无桶锁概念是差全局锁低并发场景可用D. 双 map读用 CHM写先算好再 putIfAbsent是是好计算耗时长时更优我们选 B把递归展开成两阶段public class RuleResolver { private final ConcurrentHashMapString, Rule cache new ConcurrentHashMap(); public Rule resolve(String ruleId) { Rule cached cache.get(ruleId); // 快路径直接读无锁 if (cached ! null) { return cached; } // 慢路径先把整条继承链展平全程不碰 map DequeRawRule chain new ArrayDeque(); SetString visited new HashSet(); // 防止配置里出现环 String cur ruleId; while (cur ! null) { if (!visited.add(cur)) { throw new IllegalStateException(rule inherit cycle detected at: cur); } RawRule raw ruleRepository.load(cur); chain.push(raw); // 压栈出栈时就是从祖先到子孙的顺序 cur raw.getParentId(); } // 从最顶层祖先开始逐层构建每次只做一个 key 的 putIfAbsent不嵌套 Rule parent null; Rule result null; while (!chain.isEmpty()) { RawRule raw chain.pop(); Rule built Rule.build(raw, parent); // putIfAbsent 里没有用户代码不存在递归风险 Rule prev cache.putIfAbsent(raw.getId(), built); result (prev ! null) ? prev : built; parent result; } return result; } }改动的核心思路只有一句不要在computeIfAbsent的 lambda 里碰同一个 map。展平之后每次 map 操作都是原子的单 key 操作中间不夹用户代码。顺带加了两个之前没有的东西visited集合检测继承环。原来的递归版本如果配置里出现 A→B→A会栈溢出。展平版本会抛一个业务语义清晰的异常运营在配置页面就能看到错在哪。putIfAbsent的返回值判断。并发场景下可能两个线程同时构建同一条链putIfAbsent返回非 null 说明别人先放进去了用别人的那份保证同一个 ruleId 全局只有一个 Rule 实例。顺便说说 CHM 从 JDK 7 到 8 到底改了什么既然聊到桶锁把演进也捋一遍这是面试高频但很多人只记得分段锁没了维度JDK 7JDK 8数据结构Segment 数组 HashEntry 数组 链表Node 数组 链表 红黑树锁粒度Segment默认 16 段一段锁住一批桶单个桶的首节点锁实现Segment 继承 ReentrantLocksynchronized CAS并发度固定 16由 concurrencyLevel 决定之后不变等于桶数量随扩容增长空桶写入需要加锁CAS 无锁写入casTabAtsize()遍历 Segment 求和重试 3 次不行就全锁baseCountCounterCell[]分散计数链表转树无链表长度 ≥8 且表长 ≥64 转红黑树有两点经常被误解我在评审里纠正过不止一次JDK 8 用 synchronized 是性能倒退——不是。JDK 6 之后synchronized有偏向锁/轻量级锁优化无竞争时开销极低而且这里锁的是单个桶的首节点竞争概率远低于 JDK 7 的段锁。真正的收益在空桶 CAS 写入新 key 落到空桶时完全不加锁这在稀疏 map 上快得多。链表长度到 8 就转红黑树——不完整。还有个条件表长必须 ≥64。看treeifyBin源码private final void treeifyBin(NodeK,V[] tab, int index) { NodeK,V b; int n; if (tab ! null) { if ((n tab.length) MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY 64 tryPresize(n 1); // 表太小先扩容而不是转树 else if ((b tabAt(tab, index)) ! null b.hash 0) { // ... 真正转成 TreeBin } } }表长小于 64 时宁可扩容也不转树——因为小表上链表长纯粹是容量不够导致的扩容一下就散开了转树反而增加内存和维护成本。复盘数字指标事故时修复后配置刷新任务耗时永不结束卡死 6 小时后重启平均 340ms1200 条规则单核 CPU 占用100%持续峰值 11%瞬时服务整体 CPU27%8%恢复常态配置生效延迟无穷大配置卡在旧版本 6 小时≤5 分钟一个刷新周期继承环配置的表现StackOverflowError明确异常 告警运营可自查有一个隐性损失当时没算进去那 6 小时里所有依赖这个服务的下游拿到的都是旧配置。因为解析线程卡住了但缓存里的老数据还在读接口一切正常。运营那天上线的促销规则实际上一直没生效是业务方来问为什么活动没开始才被发现的。服务看起来是好的比服务挂了更危险这句话我现在写在团队 wiki 的第一页。我的判断computeIfAbsent的 lambda 里不要做三件事不要访问同一个 map递归风险、不要做 IO/RPC会长时间持有桶锁阻塞同桶的其他 key、不要抛业务异常异常会让占位节点被清理但你的重试逻辑可能没考虑这一点。这三条我们已经写进 code review checklist。CHM 当本地缓存要慎重。它没有过期、没有容量上限、没有淘汰策略。我见过太多先用 CHM 顶一下最后顶成 OOM 的。需要缓存语义就直接上 CaffeineCaffeine.newBuilder().maximumSize(10_000).expireAfterWrite(10, MINUTES)三行的事而且它的get(key, mappingFunction)内部也做了递归保护。什么时候 CHM 仍是最优解key 集合有界且已知比如枚举、配置项、类型注册表、只增不删、读远多于写。这种场景下 CHM 的读性能是无锁的任何缓存框架都比不过。JDK 8 该升了。这个 bug 只是众多例子之一。如果实在升不动大版本至少要知道自己踩在哪些已知坑上——把项目里所有computeIfAbsent搜一遍检查 lambda 里有没有碰同一个 map这件事十分钟就能做完。思考题如果把示例中的computeIfAbsent换成merge同样的递归调用会发生什么源码上有区别吗ReservationNode的 hash 值是RESERVED -3。CHM 里还有MOVED -1ForwardingNode和TREEBIN -2。为什么这些特殊节点都用负数普通节点的 hash 为什么一定是非负的上面的展平方案里两个线程同时解析同一条继承链会重复Rule.build。如果build很贵比如要编译表达式该怎么改才能既避免递归又避免重复计算你在 JDK 8 上还踩过哪些升级到 9 就消失的坑评论区见。

相关新闻

Hanselman.Forms发布指南:Android与iOS应用商店上架全攻略

Hanselman.Forms发布指南:Android与iOS应用商店上架全攻略

Hanselman.Forms发布指南:Android与iOS应用商店上架全攻略 【免费下载链接】Hanselman.Forms The most awesome Hanselman app 项目地址: https://gitcode.com/gh_mirrors/ha/Hanselman.Forms Hanselman.Forms是一款功能强大的跨平台应用,本指南将…

2026/8/11 19:55:13 阅读更多 →
各种坐标系

各种坐标系

2026/8/11 19:55:13 阅读更多 →
2026知漫剧深度测评:面向短剧创作者的日常落地实用分析

2026知漫剧深度测评:面向短剧创作者的日常落地实用分析

测评地址:jj.jiaxunai.cn2026年AI内容生产工具全面普及,短剧、漫剧创作者普遍面临核心问题:AI成片工具能否真正接入常态化创作流程,实现降本增效?知漫剧主打文本一键生成漫剧视频,主打轻量化、高效率出片&a…

2026/8/11 19:55:13 阅读更多 →

最新新闻

解决emacs-format-all-the-code常见问题:格式化失败、找不到工具等难题一网打尽

解决emacs-format-all-the-code常见问题:格式化失败、找不到工具等难题一网打尽

解决emacs-format-all-the-code常见问题:格式化失败、找不到工具等难题一网打尽 【免费下载链接】emacs-format-all-the-code Auto-format source code in many languages with one command 项目地址: https://gitcode.com/gh_mirrors/em/emacs-format-all-the-co…

2026/8/11 20:35:26 阅读更多 →
如何解决OpenArk内核模式加载失败的技术迷宫

如何解决OpenArk内核模式加载失败的技术迷宫

如何解决OpenArk内核模式加载失败的技术迷宫 【免费下载链接】OpenArk The Next Generation of Anti-Rookit(ARK) tool for Windows. 项目地址: https://gitcode.com/GitHub_Trending/op/OpenArk 当你满怀期待地打开OpenArk这款强大的Windows内核分析工具,准…

2026/8/11 20:35:26 阅读更多 →
从现象定位风控:0 播放、低流量池、频繁人机验证,背后平台风控信号全拆解

从现象定位风控:0 播放、低流量池、频繁人机验证,背后平台风控信号全拆解

做短视频、图文自媒体最容易陷入误区:数据出问题只会反复改标题、换封面,却分不清是内容质量问题,还是平台风控拦截。不同异常数据,对应平台完全不同维度的风控逻辑,找不准根源再怎么优化内容也难起量。下面拆解三类高…

2026/8/11 20:35:26 阅读更多 →
5个关键功能:英雄联盟智能助手如何彻底改变你的游戏决策方式?

5个关键功能:英雄联盟智能助手如何彻底改变你的游戏决策方式?

5个关键功能:英雄联盟智能助手如何彻底改变你的游戏决策方式? 【免费下载链接】League-Toolkit An all-in-one toolkit for LeagueClient. Gathering power 🚀. 项目地址: https://gitcode.com/gh_mirrors/le/League-Toolkit 还在为英…

2026/8/11 20:35:26 阅读更多 →
web-basics核心组件解析:提升前端开发效率的关键技巧

web-basics核心组件解析:提升前端开发效率的关键技巧

web-basics核心组件解析:提升前端开发效率的关键技巧 【免费下载链接】web-basics 大厂前端需要掌握的JS基础能力,大厂场景题、大厂面试真题欢迎提issue和PR来丰富场景题。 项目地址: https://gitcode.com/gh_mirrors/we/web-basics web-basics是…

2026/8/11 20:35:26 阅读更多 →
Linux 内核源码分析与内存管理机制:先划清数据、调用与失败边界

Linux 内核源码分析与内存管理机制:先划清数据、调用与失败边界

Linux 内核源码分析与内存管理机制:先划清数据、调用与失败边界 深入分析 Linux 内核内存管理源码时,面临的主要挑战在于庞大的代码规模。mm/ 目录下包含数百个源文件,涵盖物理内存分配、虚拟内存映射、TLB 刷新、反向映射(RMAP&a…

2026/8/11 20:34:26 阅读更多 →

日新闻

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南 【免费下载链接】video2x A machine learning-based video super resolution and frame interpolation framework. Est. Hack the Valley II, 2018. 项目地址: https://gitcode.com/GitHub_Trending/vi/v…

2026/8/11 0:00:02 阅读更多 →
前后端分离项目中控制台与接口工具数据差异排查指南

前后端分离项目中控制台与接口工具数据差异排查指南

1. 问题现象解析:控制台与Apifox的数据差异 最近在调试一个前后端分离项目时,遇到了一个典型问题:后端服务在本地开发环境控制台能正常输出查询数据,但通过Apifox测试时却返回空结果。这种"控制台有数据,接口工具…

2026/8/11 0:00:03 阅读更多 →
AI编程实战:从Claude Code踩坑到游戏开发入门

AI编程实战:从Claude Code踩坑到游戏开发入门

1. 从“AI能帮我做游戏”到“AI让我重新学编程”最近身边不少朋友,尤其是一些非技术背景、但对游戏开发有浓厚兴趣的朋友,都在问我同一个问题:“听说现在用Claude Code这种AI编程工具,小白也能做游戏了,是真的吗&#…

2026/8/11 0:00:03 阅读更多 →

周新闻

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁 【免费下载链接】baidupankey 在线查询网盘提取码(维护中 rm repo) 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 你是否曾经在深夜寻找一份重要资料&#x…

2026/8/11 1:08:05 阅读更多 →
如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/11 1:08:05 阅读更多 →
收藏!小白程序员轻松入门大模型,从Harness工程开始实践

收藏!小白程序员轻松入门大模型,从Harness工程开始实践

文章强调学习大模型不应只关注模型本身,而应重视模型外的系统搭建,即Harness。提出AgentModelHarness的实用公式,详细介绍Harness的四个层次:持久化层、执行层、控制层和观察与验证层。文章还探讨了上下文工程、工具设计、AGENTS.…

2026/8/11 1:08:05 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/11 17:09:45 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/11 1:08:06 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/11 17:09:45 阅读更多 →