CompletionService 是组合 Executor 与阻塞队列的包装器,用于按完成顺序获取任务结果;适用于需尽快响应已完成任务的场景,如爬虫聚合、批量 RPC 调用等。

CompletionService 是什么,什么时候该用它
CompletionService 本身不执行任务,它只是个包装器,把 Executor 和任务结果队列(通常是阻塞队列)组合起来,让你能按“谁先完成谁先取”的顺序消费结果。不是所有异步场景都需要它——如果你只关心全部完成再统一处理,用 invokeAll() 或 CountDownLatch 更直接;但当你需要**尽快响应已完成任务、避免空等慢任务拖累整体吞吐**,比如爬虫聚合、批量 RPC 调用、超时熔断判断,CompletionService 就很合适。
它内部默认用 LinkedBlockingQueue 存放 Future,所以构造时传入的 Executor 必须是线程安全的,且最好用 ThreadPoolExecutor 显式配置,别用 Executors.newCachedThreadPool() 这类隐藏风险的工厂方法。
如何正确创建和提交任务
CompletionService 没有无参构造函数,必须传一个 Executor。推荐用带显式队列的实现:
ExecutorService executor = new ThreadPoolExecutor(
4, 8, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
CompletionService cs = new ExecutorCompletionService<>(executor);
提交任务用 submit(),不是 execute() —— 后者不返回 Future,CompletionService 就没法包装结果:
- 提交
Callable:直接用cs.submit(() -> doWork()) - 提交
Runnable+ 结果:用cs.submit(runnable, result),适合固定返回值场景 - 不要重复提交同一
Callable实例:它可能被多次执行,因为CompletionService不做去重
怎么安全地获取完成结果
核心是用 take() 或 poll(),区别在于阻塞行为:
-
take():阻塞直到有结果,适合“必须等至少一个完成”的场景,但要注意调用线程可能永远卡住(如果所有任务都失败且没设超时)
-
poll():立即返回,没结果就返回 null,适合轮询或结合 System.nanoTime() 做手动超时控制
- 务必在
try-catch 中调用 future.get():即使任务已标记完成,get() 仍可能抛 ExecutionException(封装了原始异常)或 CancellationException
take():阻塞直到有结果,适合“必须等至少一个完成”的场景,但要注意调用线程可能永远卡住(如果所有任务都失败且没设超时)poll():立即返回,没结果就返回 null,适合轮询或结合 System.nanoTime() 做手动超时控制try-catch 中调用 future.get():即使任务已标记完成,get() 仍可能抛 ExecutionException(封装了原始异常)或 CancellationException
典型用法:
for (int i = 0; i < tasks.size(); i++) {
Future future = cs.poll(); // 或 take()
if (future != null) {
try {
String result = future.get(); // 这里才真正取值
System.out.println(result);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
// 处理原始异常,比如 IOException、RuntimeException
}
}
}
常见陷阱和性能注意点
CompletionService 的队列只存 Future,不是结果本身。如果任务返回大对象(比如 byte[] 或集合),且你长期不调用 get(),这些对象会一直被 Future 引用,造成内存堆积——尤其当任务数多、结果大、消费慢时。
立即学习“Java免费学习笔记(深入)”;
- 别在循环里只
take()却不get():这会让Future堆在队列中,GC 不掉结果对象 - 不用
shutdownNow()后继续submit():会抛RejectedExecutionException,但更隐蔽的问题是已提交未执行的Future可能永远卡在队列里 -
ExecutorCompletionService不是线程安全的“服务”,它的线程安全性完全依赖底层Executor和队列;自己扩展时别假设它支持并发submit()+take()无竞争
最易被忽略的一点:任务完成顺序 ≠ 提交顺序,也 ≠ 线程执行顺序。如果你依赖结果顺序,得自己加序号字段或用 Map 关联,CompletionService 本身不提供这个能力。










