executorcompletionservice 是一个“结果收件箱”,解决传统 executorservice 中 future.get() 顺序阻塞和结果与完成时间错配问题,实现执行与消费解耦。

ExecutorCompletionService 是什么,它解决的到底是什么问题
它不是线程池,也不是 Future 工厂,而是一个“结果收件箱”——任务一完成,结果就自动塞进队列里,你调用 take() 或 poll() 就能立刻拿到最先完成的那个 Future。核心要解决的,就是传统 ExecutorService.submit() + 遍历 Future.get() 带来的两个硬伤:
-
阻塞卡死:哪怕第 2 个任务 100ms 就完成了,但你得等第 1 个任务(可能耗时 5s)的
get()返回后,才能轮到它 - 顺序错配:结果处理顺序完全绑定提交顺序,和实际完成时间毫无关系
用一句话说:ExecutorCompletionService 把“任务执行”和“结果消费”解耦了,就像快递柜——谁先送到,谁先进柜,你扫码开柜拿的永远是最新到的那一单。
怎么初始化和提交任务,关键参数别写错
它必须包装一个已有的 ExecutorService,不能单独 new;内部默认用 LinkedBlockingQueue 存已完成的 Future,但你可以传自定义队列(比如限制容量防 OOM):
- 正确写法:
new ExecutorCompletionService(executor),其中executor必须是非空、可运行的线程池 - 错误写法:
new ExecutorCompletionService(null)→NullPointerException - 提交任务只支持
submit(Callable)或submit(Runnable, result);不支持纯Runnable(因为没返回值,进不了结果队列) - 如果用
submit(Runnable, T),第二个参数是任务完成时直接返回的固定值,适合“执行即成功,结果无差异”的场景
示例:completionService.submit(() -> { Thread.sleep(300); return "done"; }) —— 这才是最常用形态。
take() 和 poll() 怎么选,超时控制怎么加
take() 和 poll() 看似相似,但行为差异直接影响程序健壮性:
-
take():队列空就一直阻塞,直到有任务完成。适合“必须等结果”的主流程,但若所有任务都失败或卡住,线程会永久挂起 -
poll():立即返回,没结果就返null,适合非关键路径或需主动控制等待逻辑的场景 -
poll(long timeout, TimeUnit unit):最实用,设个合理超时(比如 30s),避免无限等;超时后可做兜底(重试、告警、跳过)
注意:take() 和 poll() 都会从队列中移除元素,不可重复消费;且它们只管“有没有完成”,不管“完成是否成功”——异常任务也会入队,get() 时才会抛 ExecutionException。
常见坑:任务异常了,结果还在队列里?
是的,这是最容易忽略的一点:ExecutorCompletionService 不过滤异常任务,只要 Callable.call() 执行完了(哪怕抛了异常),对应的 Future 就会被放进队列。所以你 take().get() 时,大概率直接触发异常,而不是拿到预期结果。
- 必须对
get()做try-catch,捕获ExecutionException和InterruptedException - 别在
catch里简单吞掉异常——要记录日志、判断是否可重试、或标记该任务失败 - 如果想跳过失败任务继续取下一个,得用循环 +
poll()+ 异常检查,不能依赖take()自动跳过
真正复杂的不是怎么用它,而是怎么安全地从队列里把“成功结果”和“失败痕迹”区分开——这点文档不讲,但线上出问题十次有八次栽在这儿。









