如何在 Java 中使用 ExecutorCompletionService 按照异步任务完成的先后顺序获取返回结果
如何在 Ja va 中使用 ExecutorCompletionService 按照异步任务完成的先后顺序获取返回结果

免费影视、动漫、音乐、游戏、小说资源长期稳定更新! 👉 点此立即查看 👈
处理异步任务时,你是否遇到过这样的困扰:提交了一堆任务,却只能按照提交顺序一个个等待结果,即便后面的任务先完成了也得干等着?这在处理网络请求或I/O操作时尤其低效。好在Ja va并发包里藏着一个利器——ExecutorCompletionService。它能帮你按任务实际完成的先后顺序获取结果,真正做到“谁先做完,谁先汇报”。
简单来说,它是对普通Executor的增强包装,核心秘诀在于内部用了一个BlockingQueue(默认是LinkedBlockingQueue)来暂存已完成的Future。这样一来,获取结果的顺序就与提交或执行的顺序脱钩了。
ExecutorCompletionService 按任务实际完成先后顺序返回结果,其核心是用BlockingQueue暂存已完成Future,通过done回调自动入队,take()/poll()按完成顺序出队获取Future再调用get()得结果。
核心原理:完成即入队,取即出队
它的工作机制非常清晰,就像一个高效的流水线分拣系统:
- 当你通过
submit()提交一个Callable任务后,ExecutorCompletionService会将其交给底层的Executor去执行。 - 与此同时,它会创建一个封装该任务的
Future,并巧妙地注册一个内部监听机制(这通常依赖于FutureTask的done状态回调)。 - 一旦任务执行完毕(无论是成功返回还是抛出异常),对应的
Future就会被自动放入内部的阻塞队列中等待被领取。 - 这时,你调用
take()或poll()方法,就能按照任务完成的先后顺序,从队列中取出这些Future,之后再调用get()便能拿到最终的结果或异常。
整个过程,实现了从“任务完成”到“结果可被消费”的无缝、有序衔接。
基本使用步骤(带示例)
光说不练假把式。假设我们有3个模拟的异步任务,它们的耗时各不相同(以此来模拟现实中网络或I/O操作的差异),我们的目标就是谁能干完活,就先处理谁的结果。
(以下为关键代码逻辑,可直接运行)
- 第一步:准备线程池。 例如,使用
Executors.newFixedThreadPool(3)创建一个固定大小的线程池。 - 第二步:包装成CompletionService。 用这个线程池作为参数,构造一个
ExecutorCompletionService实例。 - 第三步:提交任务。 循环提交你的
Callable任务(比如,每个任务休眠随机时间后返回一条耗时信息)。 - 第四步:按完成顺序获取。 关键来了!调用
completionService.take()。这个方法会阻塞当前线程,直到有任何一个任务完成,然后立刻返回那个最先完成的Future。 - 第五步:提取结果。 对返回的
Future调用get()。由于这个Future对应的任务已经确定完成,此时调用get()会立即返回结果,不会发生阻塞。
通过这五步,你就能轻松实现“结果先到先得”,极大提升了处理效率。
处理异常与超时的注意事项
现实世界不会总是一帆风顺,任务可能失败,我们也不想无限期等待。使用ExecutorCompletionService时,有几点需要特别留意:
立即学习“Ja va免费学习笔记(深入)”;
- 异常处理仍在。 从
CompletionService取出的Future,调用其get()方法时,仍然可能抛出ExecutionException(它包装了任务执行时抛出的原始异常)或InterruptedException,因此必要的try-catch不可少。 - 非阻塞与超时选项。 如果不想让
take()一直阻塞,可以使用poll()方法(没有已完成任务则立即返回null),或者使用poll(long timeout, TimeUnit unit)来设置一个等待超时时间。 - 分清Future和结果。 务必记住:
take()和poll()返回的是Future对象,并不是最终的结果值。你必须再调用一次get(),才能拿到实际的计算结果或感知到执行异常。 - 失败任务的处理。 如果某个任务执行失败,而你想忽略它继续处理其他已完成的任务,可以在调用
get()时捕获ExecutionException,然后根据业务逻辑决定是跳过、记录还是重试。一个任务的失败不会阻塞队列,其他已完成的任务依然可以被正常取出。
对比 Future 列表 + 循环 isDone() 的劣势
在没有CompletionService之前,一个常见的替代方案是:将提交任务后得到的所有Future保存到一个列表里,然后写一个循环去不停地轮询每个Future的isDone()方法。
这种方法存在几个明显的短板:效率低下(轮询本身消耗CPU)、响应不实时(从任务完成到被轮询发现存在延迟)、容易写出忙等待(busy-wait)代码,并且线程安全性需要额外小心。
反观ExecutorCompletionService,它基于阻塞队列的“生产者-消费者”模型,天然实现了“完成即通知”的机制。消费线程在无任务时安心阻塞,有任务时立刻被唤醒,没有任何空转开销。同时,其内部封装保证了线程安全,使用语义也清晰直观——这就是专业工具带来的降维打击。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
使用Python合并与拆分Excel单元格的实用方法
使用Python合并与拆分Excel单元格的实用方法 处理Excel表格时,合并单元格是个绕不开的操作。无论是为了制作清晰美观的表头,还是为了突出显示某些关键信息,这个功能都相当实用。不过,当需要批量处理或者将流程自动化时,手动在Excel里点点划划就有点力不从心了。今天,我们就来聊聊如何用Pyth
SpringBoot OpenFeign整合okHttpClient实践
前言 在SpringCloud微服务架构中,服务间的数据传输,OpenFeign无疑是那个既简单又好用的选择。不过,它默认使用的客户端是JDK自带的HttpURLConnection,这里有个小细节值得注意:这个客户端本身并不具备连接池功能。 这意味着什么?简单来说,每一次发起远程调用,系统都会尝试
修改JAR文件并重新打包的两种方式
本文介绍两种修改 JAR 包内文件(如配置文件或 Class 文件)后重新打包的方式:Ja va 命令方式 与 Ant 脚本方式。 核心警告 对于 Spring Boot 的可执行 JAR 包,重新打包时严禁使用压缩(必须使用存储模式),否则会导致 ClassNotFoundException 或启
C++中INI配置文件读取技术详解
一、INI文件格式概述 在众多配置文件格式中,INI(Initialization)格式堪称经典。它以纯文本形式存储,结构清晰直观,既便于开发者手动编辑与维护,也易于程序进行自动化解析与读取。这种简单高效的特点,使其在软件配置、游戏设置、系统参数管理等场景中,至今仍被广泛应用。 1 1 基本结构 一
idea如何保存当前已修改的文件|恢复到未修改状态
1、打开git,如下步骤1 先来看第一张图,这是整个操作的起点。 在步骤2的区域,你会看到所有被修改过的文件都列在这里,一目了然。 而步骤3指向的代码区域,正是我们修改后还在报错的部分,问题就出在这儿。 这里有个关键细节:注意看圈4标识的地方,你所有修改过的代码行,IDE都会用淡绿色的背景高亮显示,
- 日榜
- 周榜
- 月榜
1
2
3
4
5
6
7
8
9
10
相关攻略
2015-03-10 11:25
2015-03-10 11:05
2021-08-04 13:30
2015-03-10 11:22
2015-03-10 12:39
2022-05-16 18:57
2025-05-23 13:43
2025-05-23 14:01
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

