You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java多线程下如何避免相同入参的高开销方法重复执行并缓存结果

解决方案

方案1:基于锁注册表+ConcurrentHashMap实现细粒度锁

核心思路是单独维护锁池,保证内容相等的入参始终对应同一个锁对象,同时规避字符串驻留、不同实例内容相同的锁冲突问题。缓存统一使用线程安全的ConcurrentHashMap,无需额外处理HashMap的并发安全问题。

// 结果缓存,线程安全
private final ConcurrentHashMap<String, Object> resultCache = new ConcurrentHashMap<>();
// 锁池:存储每个入参对应的锁对象,用WeakReference避免内存泄漏
private final ConcurrentHashMap<String, WeakReference<Object>> lockPool = new ConcurrentHashMap<>();

public Object doSomethingExpensive(String x) {
    // 优先查缓存,命中直接返回
    Object cachedResult = resultCache.get(x);
    if (cachedResult != null) {
        return cachedResult;
    }

    // 获取当前入参对应的锁对象,相同入参必然拿到同一个锁
    Object lock = getLockForKey(x);
    synchronized (lock) {
        // 二次校验:避免等待锁的过程中已有其他线程完成计算写入缓存
        cachedResult = resultCache.get(x);
        if (cachedResult != null) {
            return cachedResult;
        }
        // 执行高开销计算
        Object result = doActualExpensiveOperation(x);
        // 写入缓存
        resultCache.put(x, result);
        return result;
    }
}

// 从锁池获取对应key的锁,保证相同key返回同一个锁实例
private Object getLockForKey(String key) {
    // 循环重试,避免WeakReference被GC回收的并发问题
    while (true) {
        WeakReference<Object> ref = lockPool.get(key);
        if (ref != null) {
            Object lock = ref.get();
            if (lock != null) {
                return lock;
            }
            // 引用已被GC,移除旧条目
            lockPool.remove(key, ref);
        }
        // 生成新锁,用putIfAbsent保证并发下相同key只会存同一个锁实例
        Object newLock = new Object();
        ref = new WeakReference<>(newLock);
        WeakReference<Object> existing = lockPool.putIfAbsent(key, ref);
        if (existing == null) {
            return newLock;
        }
        // 其他线程已提前写入锁,优先用已存在的
        Object existingLock = existing.get();
        if (existingLock != null) {
            return existingLock;
        }
        // 其他线程写入的锁已被GC,重试循环
        lockPool.remove(key, existing);
    }
}

// 原有的高开销计算逻辑
private Object doActualExpensiveOperation(String x) {
    // 你的业务逻辑实现
}

该方案优势:

  • 锁粒度仅限制相同入参的请求,不同入参请求完全不会互斥,性能远高于全局锁
  • 锁对象是独立生成的Object实例,和入参本身的实例、字符串驻留池完全无关,只要入参内容相等就能拿到同一个锁
  • 锁池用WeakReference包装锁对象,当没有线程持有该锁时,GC可自动回收锁对象,避免锁池无限膨胀导致内存泄漏
  • 双重校验机制完全避免重复执行高开销计算逻辑

方案2:基于FutureTask的更简洁实现

如果不想手动维护锁池,可以直接利用ConcurrentHashMap的原子写入特性结合FutureTask实现,代码更精简:

private final ConcurrentHashMap<String, Future<Object>> resultCache = new ConcurrentHashMap<>();

public Object doSomethingExpensive(String x) throws ExecutionException, InterruptedException {
    while (true) {
        Future<Object> future = resultCache.get(x);
        if (future == null) {
            Callable<Object> callable = () -> doActualExpensiveOperation(x);
            FutureTask<Object> futureTask = new FutureTask<>(callable);
            // 原子性写入,相同key只会有一个FutureTask被成功写入
            future = resultCache.putIfAbsent(x, futureTask);
            if (future == null) {
                future = futureTask;
                futureTask.run();
            }
        }
        try {
            // 等待计算完成返回结果,相同入参的后续请求会自动阻塞在此处复用结果
            return future.get();
        } catch (CancellationException e) {
            // 任务被取消时移除缓存重试
            resultCache.remove(x, future);
        }
    }
}

该方案优势:

  • 无需单独维护锁池,代码逻辑更简洁
  • 自动实现相同入参请求的等待复用逻辑,无需手动加锁
  • 同样保证不同入参的请求互不影响

通用说明

  • 如果需要支持缓存过期、容量淘汰等策略,可以直接把ConcurrentHashMap替换成Guava Cache、Caffeine等成熟缓存组件,核心逻辑无需改动
  • 如果入参是自定义对象,只需保证自定义对象正确重写了equals()和hashCode()方法,即可直接替换上述代码中的String类型入参

内容的提问来源于stack exchange,提问作者Robin Kreuzer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 18:15:06