concurrenthashmap.computeifabsent 不是完全原子的,它仅保证映射函数不被多线程重复执行,但不保证“读-计算-写”全过程原子;若函数返回 null,则缓存无值且无异常;若抛异常,jdk 8 中 key 会残留为“正在计算”状态致后续调用阻塞,jdk 9+ 则清除该 key。

ConcurrentHashMap.computeIfAbsent 为什么不是完全原子的
它只保证“计算函数不被多个线程重复执行”,但不保证整个读-算-写过程原子——也就是说,如果 computeIfAbsent 的 mappingFunction 返回 null,结果就是缓存里没值,且无异常;如果函数抛异常,该 key 会留在 map 中(状态为“正在计算”),后续调用会阻塞等待,直到异常传播出去或成功返回。
- 常见错误现象:
computeIfAbsent内部抛NullPointerException,但调用方没 catch,导致线程卡住、CPU 占用突增 - 使用场景:适合轻量、无副作用、幂等的构建逻辑(比如 new 一个对象、解析固定字符串)
- 参数差异:
computeIfAbsent(key, func)中func不能返回null,否则 map 不存值,也不报错——这容易让人误以为“没触发计算” - 性能影响:若
func耗时长(如 IO 或复杂计算),会阻塞其他线程对同一 key 的访问,拖慢整体并发吞吐
替代方案:什么时候该用 computeIfAbsent,什么时候该换
它不是万能缓存工具,本质是“懒加载 + 线程安全初始化”,不是“带过期/淘汰的缓存”。真要实现并发缓存,得配合外部控制逻辑。
- 适合直接用:
ConcurrentHashMap存单例对象(如Pattern.compile("xxx"))、配置映射(如Locale→DateTimeFormatter) - 不适合直接用:需要 TTL、最大容量、自动刷新、异步加载的场景——这时
computeIfAbsent无法满足,硬套反而掩盖问题 - 兼容性注意:Java 8 引入,低版本不可用;Java 9+ 对
mappingFunction抛出的 checked exception 处理更严格(会包装成RuntimeException) - 一个简短示例:
cache.computeIfAbsent(key, k -> { // 这里不能 return null,也不能 sleep / db 查询 return new ExpensiveObject(k); });
容易踩的坑:null、异常、重入都可能让缓存失效或死锁
很多人以为写了 computeIfAbsent 就高枕无忧,其实几个边界情况非常隐蔽。
-
mappingFunction返回null→ key 对应值为null,下次 get 还是null,但不会再次调用函数(看似“缓存命中”,实则没缓存) - 函数内调用本 map 的其他方法(比如又调
get或computeIfAbsent)→ 可能引发死锁(JDK 8 中已修复部分 case,但嵌套深仍风险高) - 函数抛出未检查异常(
RuntimeException)→ 当前线程失败,其他等待线程也会收到同样异常,key 会被清除(JDK 9+ 行为),但 JDK 8 是不清除的,导致后续调用持续阻塞 - key 本身是可变对象(比如自定义类没重写
equals/hashCode)→ 同一逻辑 key 可能被算作多个 key,缓存击穿
真正安全的并发缓存,往往要加一层封装
直接裸用 computeIfAbsent 做缓存,等于把并发控制、生命周期、可观测性全推给业务代码。复杂点在于:你得自己管“计算中状态”“失败重试”“是否允许空值”这些细节。
立即学习“Java免费学习笔记(深入)”;
- 简单封装建议:用
AtomicReference包一层结果,把null和“未计算”区分开 - 更稳妥做法:用
Caffeine的CacheLoader,它明确区分load(可抛异常、支持异步)、refresh、expireAfterWrite等语义 - 最容易被忽略的地方:缓存 key 的相等性判断和 value 的线程安全性——
computeIfAbsent不帮你校验 value 是否可共享,如果返回的是非线程安全对象(如SimpleDateFormat),并发用就出事










