concurrenthashmap.computeifabsent通过synchronized+cas将检查与插入合为原子操作,避免重复计算;适用于低频、短期、可控key场景,但无过期/淘汰机制,不适用需超时或lru的缓存。

ConcurrentHashMap.computeIfAbsent 为什么能避免重复计算
它把“检查是否存在”和“不存在时创建并放入”这两步合成了一个原子操作,底层用 synchronized + CAS 实现,不会出现两个线程同时发现 key 不存在、然后都去执行 value 生成逻辑的问题。
常见错误现象:NullPointerException 或重复初始化(比如缓存对象被构造两次、数据库查询跑了两遍)——往往是因为先 get() 判断再手动 put(),中间有竞态窗口。
- 只在 value 创建开销大(如 IO、复杂计算)时才值得用
computeIfAbsent - 传入的 mappingFunction 不能为
null,否则抛NullPointerException - mappingFunction 内部不应依赖外部可变状态,否则可能因重入导致不一致
- 如果 mappingFunction 抛异常,该次调用失败,key 不会存入 map,但异常会向上抛出
computeIfAbsent 的参数陷阱:lambda 捕获变量要小心
Java 中 lambda 表达式捕获的局部变量必须是 effectively final;但更隐蔽的问题是:若捕获的是可变引用(比如一个 StringBuilder),多个线程并发调用时可能读到彼此修改后的中间状态。
使用场景:缓存 JSON 解析结果、模板渲染、配置加载等需要按需构建的值。
立即学习“Java免费学习笔记(深入)”;
- 避免在 lambda 里直接使用非 final 的集合、builder、计数器等可变对象
- 如果必须复用资源,应在线程内新建(如
() -> new ObjectMapper().readValue(json, T.class)) - 不要在 lambda 中调用含副作用的方法(如写日志、发请求),除非你明确接受它可能被执行多次(异常时重试或其它线程触发)
示例:
cache.computeIfAbsent(key, k -> {
// ✅ 安全:每次调用都新建解析器
return objectMapper.readValue(fetchJson(k), Data.class);
});
ConcurrentHashMap 作为缓存的边界在哪
它不是通用缓存解决方案,没有过期、淘汰、大小限制机制。适合短期、低频更新、key 空间可控的场景(如类元信息、静态配置映射)。
性能影响:高并发下 computeIfAbsent 比普通 putIfAbsent 略重,因为要执行函数;但如果函数本身耗时远大于 map 操作,这点开销可忽略。
- 不支持自动失效,需自行管理生命周期(比如配合
ScheduledExecutorService清理) - 不处理内存泄漏:value 强引用持有对象,若 value 是大对象或包含 ClassLoader,容易 OOM
- key 和 value 都不能为
null,否则抛NullPointerException - 如果 key 是自定义对象,务必正确实现
equals()和hashCode()
替代方案对比:什么时候不该用 computeIfAbsent
当你要做带超时、LRU、异步加载、统计命中率等事时,ConcurrentHashMap 就力不从心了。这时候该换真正缓存库。
常见错误现象:用 computeIfAbsent 包裹 HTTP 请求,结果缓存无限增长、无法降级、超时不生效。
- 需要定时刷新 → 考虑
Caffeine.newBuilder().refreshAfterWrite(10, TimeUnit.MINUTES) - 需要容量控制 →
Caffeine.newBuilder().maximumSize(1000) - 需要异步加载且不阻塞调用线程 →
AsyncLoadingCache - 只是临时避免重复构造,且生命周期明确 →
computeIfAbsent仍是最轻量靠谱的选择
最易被忽略的一点:computeIfAbsent 的 mappingFunction 在 key 第一次插入时才执行,之后所有 get 都是无锁读 —— 这个“首次”时机,取决于哪个线程最先触发它,而不是你预想的“应用启动时”。










