futuretask 本身不缓存结果,而是通过 concurrenthashmap 缓存其作为“占位符”,利用 putifabsent 保证同 key 仅触发一次计算;未命中时提交 futuretask 到线程池执行,命中则调用 get(timeout, unit) 阻塞等待,需捕获 executionexception、timeoutexception 和 interruptedexception;异常或取消后立即 remove,避免残留失效任务。

直接用 FutureTask 做“结果缓存”容易误入歧途——它本身不存结果,而是管理“一次计算任务”的生命周期。真正高性能的缓存,是利用它的阻塞等待 + 任务唯一性特性,在高并发下避免重复计算,同时让后续请求复用同一份异步执行结果。
用 ConcurrentHashMap 缓存 FutureTask 而不是结果
把 FutureTask 当作“占位符”放进线程安全的 Map,靠 putIfAbsent 的原子性确保同 key 只触发一次真实计算:
- 查缓存:map.get(key),拿到的是 FutureTask,不是最终值
- 未命中:new FutureTask
(callable),再 map.putIfAbsent(key, ft) - 插入成功者调用 ft.run() 或 submit(ft) 启动计算;其他人直接 ft.get() 阻塞等待
- 这样既避免了多个线程重复查库、调接口,又天然支持结果复用和异常传播
主线程阻塞要带超时和异常兜底
FutureTask.get() 是挂起式等待,不是忙等,但裸调用风险高:
- 必须用 get(timeout, unit),防止某次计算卡死导致整个接口不可用
- 捕获 ExecutionException(包装业务异常)、TimeoutException(降级处理)、InterruptedException(恢复中断状态)
- 不建议在 get() 前加锁或同步块——FutureTask 本身已封装状态转换与线程安全
配合线程池调度,别让 run() 在主线程执行
FutureTask.run() 是同步执行逻辑,若直接在接口线程里调用,就失去了异步意义:
- 应提交给专用线程池:executor.submit(ft),释放主线程
- 线程池用固定大小(如 CPU 核心数 × 2),拒绝策略选 CallerRunsPolicy,过载时由调用方本地执行,自然限流
- 计算完成后,可选择性写入 Guava Cache 或 Caffeine,供后续请求秒级命中,形成二级缓存结构
清理失效任务,防止缓存污染
FutureTask 状态不可逆,但异常或取消后需主动清理,否则下次请求会拿到已失败的 Future:
- catch ExecutionException 后,立即 map.remove(key, ft)
- cancel(true) 触发 CancellationException 时,同样 remove,避免残留无效占位符
- 不依赖定时清理——失效即清,保证每次 get() 拿到的都是有效或待执行的任务
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











