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
相关产品推荐
相关产品推荐

