不能只用concurrenthashmap直接存结果,因为多个线程同时发现key不存在会各自执行耗时计算,造成重复劳动和资源浪费;而futuretask+concurrenthashmap通过缓存任务而非结果,利用putifabsent原子性确保同参数仅触发一次计算,其余线程等待复用。

Java中利用FutureTask实现高并发下的单次计算缓存,核心在于用ConcurrentHashMap存Future对象,配合putIfAbsent的原子性,确保相同参数只触发一次真实计算,其余线程等待并复用结果。
为什么不能只用ConcurrentHashMap直接存结果
如果缓存直接存计算结果(如ConcurrentHashMap<k v></k>),多个线程同时发现key不存在,会各自执行耗时计算,造成重复劳动。这在高并发+高开销场景下浪费严重。
- 线程A查无结果 → 执行计算 → 写入缓存
- 线程B、C几乎同时查无结果 → 各自重复执行相同计算
- 结果虽最终一致,但CPU、IO、数据库连接等资源被白白消耗
FutureTask + ConcurrentHashMap 的协作机制
把“计算任务”本身缓存起来,而不是缓存“计算结果”。每个key对应一个FutureTask,它封装了待执行的Callable,且具备延迟启动、状态管理、异常捕获能力。
-
cache.get(arg)尝试获取已有的Future;若为null,说明没人发起过该任务 -
cache.putIfAbsent(arg, ft)原子插入:仅当key不存在时才写入新FutureTask,返回null;否则返回已存在的Future - 插入成功的线程调用
ft.run()真正触发计算;其他线程拿到已有Future,调用get()阻塞等待完成 - FutureTask自动处理call()中的异常,并通过ExecutionException包装,调用方能统一捕获
关键细节与健壮性处理
生产环境需覆盖异常、取消、重试等边界情况,否则缓存可能污染或挂起。
- 遇到
CancellationException:说明Future被取消,应立即cache.remove(arg, f)清理,避免后续请求拿到已失效的Future - 遇到
ExecutionException:计算逻辑抛出异常,同样需移除缓存,并根据业务决定是否重新抛出原始异常 - 使用
get(timeout, unit)而非无参get:防止某次计算卡死导致整个线程永久阻塞 - 不建议手动管理线程:FutureTask.run()应在合适线程中执行(如提交到线程池),避免在调用线程中同步阻塞计算
一个精简可用的模板结构
无需继承或复杂泛型,聚焦主干逻辑:
private final ConcurrentMap<string future>> cache = new ConcurrentHashMap();
private final Computable<string string> backend;
public String compute(String key) throws Exception {
while (true) {
Future<string> f = cache.get(key);
if (f == null) {
FutureTask<string> ft = new FutureTask(() -> backend.compute(key));
f = cache.putIfAbsent(key, ft);
if (f == null) f = ft;
}
try {
return f.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
cache.remove(key, f); // 超时也清理,防脏数据
throw e;
} catch (CancellationException | ExecutionException e) {
cache.remove(key, f);
throw e;
}
}
}</string></string></string></string>Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











