商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 如何在 Java 中使用 ConcurrentHashMap.computeIfAbsent() 确保在并发场景下只初始化一次缓存

如何在 Java 中使用 ConcurrentHashMap.computeIfAbsent() 确保在并发场景下只初始化一次缓存

  发布于2026-07-09 阅读(0)

扫一扫,手机访问

在并发场景下初始化缓存,ConcurrentHashMap.computeIfAbsent() 可以说是最稳妥的选择之一。它的核心价值在于:原子化地完成“检查-插入”操作,确保同一 key 的初始化逻辑只被第一个到达的线程执行一次,其余线程阻塞等待结果,从而避免经典 if-put 方式带来的竞态条件和重复初始化问题。

如何在 Ja va 中使用 ConcurrentHashMap.computeIfAbsent() 确保在并发场景下只初始化一次缓存

简单来说,ConcurrentHashMap.computeIfAbsent() 能保证:当多个线程同时往同一个 key 上塞值时,最多只有一个线程会执行初始化逻辑(也就是 mappingFunction),其他线程会等着,直到拿到这个唯一的结果。

为什么 computeIfAbsent 比 if-put 更安全

先看一段常见但存在问题的代码:

❌ 不安全(竞态条件)
if (map.get(key) == null) {
    map.put(key, createExpensiveValue()); // 多个线程可能同时进入
}

这段代码的问题在于竞态条件:线程 A 检查到 key 不存在,还没来得及 put,线程 B 也跑过来检查,发现也是 null。结果两个线程都调用了 createExpensiveValue(),不仅造成重复初始化、资源浪费,严重时还可能引发逻辑错误。

而 computeIfAbsent 将“检查”和“插入”这两个步骤合并为一个原子操作,内部加锁粒度控制在 hash 桶级别,既保证了性能,又做到了语义上的严谨。

正确使用 computeIfAbsent 初始化缓存

基本用法十分直接:

ConcurrentHashMap cache = new ConcurrentHashMap();

ExpensiveObject obj = cache.computeIfAbsent("key1", k -> new ExpensiveObject(k));

这里有几个关键点值得注意:

  • Lambda 表达式只会在 key 不存在时被调用一次,且由第一个到达的线程执行。
  • 如果 key 已存在,即使 value 为 null,也不会触发 lambda。特别提醒:ConcurrentHashMap 不允许 null value,所以返回 null 意味着 key 不存在。
  • mappingFunction 执行期间,其他线程对同一 key 的 computeIfAbsent 调用会被阻塞,直到初始化完成并返回值。这才是其精妙之处。

注意事项与常见陷阱

以下是一些容易踩坑的地方,需要特别注意:

  • mappingFunction 内部不要再次调用本 map 的 computeIfAbsent 或 compute,否则可能触发死锁——内部锁并不支持重入。
  • mappingFunction 应该尽量轻量。如果创建过程耗时或可能失败,建议包装异常或提供降级方案,避免长时间阻塞其他线程。
  • 这个方法不适合“懒加载后可变对象”的场景。computeIfAbsent 返回的是对象引用,后续修改对象状态不会影响缓存机制,但线程安全问题需自行处理。
  • 如果初始化逻辑依赖外部状态(比如数据库或远程服务),失败时应抛出 RuntimeException(computeIfAbsent 不捕获 checked exception),或者在 lambda 内部兜底返回默认值或占位符。

进阶:结合 Supplier 或缓存工厂封装

为提升代码的可读性和复用性,可以将初始化逻辑单独抽取出来:

private final ConcurrentHashMap cache = new ConcurrentHashMap();

private ExpensiveObject getOrCreate(String key) {
    return cache.computeIfAbsent(key, this::createExpensiveObject);
}

private ExpensiveObject createExpensiveObject(String key) {
    // 可加入日志、指标、重试等逻辑
    return new ExpensiveObject(key);
}

这种拆分好处很多:业务逻辑更清晰,也方便对 createExpensiveObject 方法进行单元测试。同时,如果未来需要调整初始化策略(比如增加超时、重试或者监控),改动起来也相当方便。

本文转载于:https://www.php.cn/faq/2411134.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注