CompletionService
简介
CompletionService
是一个接口,它定义了两个主要的方法:submit
和take
。submit
方法用于将任务提交给服务执行,而take
方法则用于获取已完成的任务结果。这个接口通常与ExecutorService
一起使用,但它提供了一种不同的方式来处理任务完成的通知和结果的收集。CompletionService
的一个常见实现是ExecutorCompletionService
,它基于BlockingQueue
实现了CompletionService
接口。
ExecutorCompletionService
的工作原理
ExecutorCompletionService
维护了一个内部的BlockingQueue
,用于存放任务完成后返回的Future
对象。每当一个任务完成,它的Future
会被放入队列中。这意味着我们可以按任务完成的顺序来获取结果,而不是它们被提交的顺序。
CompletionService
和CompletableFuture
的区别
CompletionService更适合于批量提交异步任务,并且只关心任务的结果,而不关心任务之间的依赖关系。例如,同时从多个数据源查询数据,返回任意一个可用的结果即可。
CompletableFuture更适合于构建复杂的异步计算流程,并且可以灵活地控制任务之间的依赖关系和执行顺序。例如,根据用户输入查询商品信息,然后根据商品信息查询库存和价格,最后显示给用户。
使用CompletionService
的步骤
- 创建线程池:首先,你需要创建一个
ExecutorService
实例,这可以通过Executors
类的静态工厂方法来完成。 - 创建
CompletionService
:然后,使用这个ExecutorService
实例来创建ExecutorCompletionService
。 - 提交任务:向
CompletionService
提交任务,这些任务会自动被分发到线程池中执行。 - 获取结果:使用
take
方法来获取完成的任务结果,这通常是按照任务完成的顺序来的。
实际案例分析
让我们来看一个具体的例子,这个例子展示了如何使用CompletionService
来处理预算科目数据。在这个场景中,我们需要从外部API获取大量的预算科目信息,并且需要按顺序处理这些信息。(根据预算科目获取预算科目详情)
/**
* 一次查询50条,判断hasNextPage属性是否为false, 否则继续查询
*/
private void getBudgetSubjectList(Map<String, String> authMap) {
int pageNo = 1;
// 创建线程池
int availabled = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(availabled * 2 + 1);
CompletionService<List<BudgetItems>> cs = new ExecutorCompletionService<>(executor);
boolean hasNextPage = true;
while (hasNextPage) {
log.info("获取预算科目, 第{}页", pageNo);
Map<String, Object> map = new HashMap<>();
map.put("pageNo", pageNo);
map.put("pageSize", 50);
String body = JSON.toJSONString(map);
// 获取预算科目详情
String resp = requestMaycurApi(authMap, body, BUDGET_SUBJECT_API);
if (resp.contains("list") && resp.contains("hasNextPage")) {
JSONObject data = JSON.parseObject(resp).getJSONObject("data");
hasNextPage = data.getBoolean("hasNextPage");
List<MaycurBudgetSubject> subjects = data.getList("list", MaycurBudgetSubject.class);
if (subjects.isEmpty()) {
log.error("获取预算科目失败,科目列表为空");
hasNextPage = false;
} else {
// 获取预算科目详情,一次最多查50个
cs.submit(() -> getBudgetSubjectDetailList(subjects, authMap));
}
} else {
hasNextPage = false;
log.error("获取预算科目,解析数据失败");
}
pageNo++;
}
try {
CountDownLatch latch = new CountDownLatch(pageNo - 1); // 创建一个计数器,初始值任务数量
for (int i = 1; i <= pageNo - 1; i++) {
Future<List<BudgetItems>> future = cs.take();
executor.execute(() -> {
log.info("异步存储科目详情");
latch.countDown(); // 计数器减1
// 存储预算科目
List<BudgetItems> budgetItems = future.get();
budgetItemsService.saveOrUpdateBatch(budgetItems);
});
}
latch.await(); // 等待计数器归0
} catch (Exception e) {
String msg = "获取预算科目失败";
log.error(msg, e);
throw new ServiceException(msg);
} finally {
executor.shutdown();
}
}
在这个例子中,我们创建了一个固定大小的线程池,并使用它来初始化ExecutorCompletionService
。对于每个页面的数据,我们都将其提交给CompletionService
。当我们调用cs.take()
时,我们会获得一个已经完成的任务,这样我们就可以立即处理结果,而不必等待所有任务都完成。
CompletableFuture
方式
/**
* CompletableFuture 方式
* 一次查询50挑,判断hasNextPage属性是否为false, 否则继续查询
*/
private void getBudgetSubjectList(Map<String, String> authMap) {
int pageNo = 1;
// 创建线程池
int availabled = Runtime.getRuntime().availableProcessors();
ExecutorService executor =
Executors.newFixedThreadPool(availabled * 2 + 1);
boolean hasNextPage = true;
List<CompletableFuture<Boolean> > futures = new ArrayList<>();
try {
while (hasNextPage) {
log.info("获取预算科目, 第{}页", pageNo);
Map<String, Object> map = new HashMap<>();
map.put("pageNo", pageNo);
map.put("pageSize", 50);
String body = JSON.toJSONString(map);
// 异步获取预算科目列表
String resp = requestMaycurApi(authMap, body, BUDGET_SUBJECT_API);
if (resp.contains("list") && resp.contains("hasNextPage")) {
JSONObject data = JSON.parseObject(resp).getJSONObject("data");
hasNextPage = data.getBoolean("hasNextPage");
List<MaycurBudgetSubject> subjects = data.getList("list", MaycurBudgetSubject.class);
if (subjects.isEmpty()) {
log.error("获取预算科目失败,科目列表为空");
hasNextPage = false;
} else {
// 异步获取预算科目详情,一次最多查50个
CompletableFuture<Boolean> future = CompletableFuture
.supplyAsync(() -> getBudgetSubjectDetailList(subjects, authMap), executor)
.thenApplyAsync(list -> {
log.info("保存预算科目详情, 共{}条", list.size());
return budgetItemsService.saveOrUpdateBatch(list);
}, executor);
future.exceptionally(e -> {
log.error("同步预算科目第{}页出现异常", map.get("pageNo"), e);
return null;
});
futures.add(future);
}
} else {
hasNextPage = false;
log.error("获取预算科目,解析数据失败");
}
pageNo++;
}
} catch (Exception e) {
String msg = "同步预算科目失败";
log.error(msg, e);
throw new ServiceException(msg);
} finally {
log.info("关闭线程池");
// 使用allOf方法等待所有CompletableFuture完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
executor.shutdown();
}
}
结论
CompletionService
是Java并发框架中的一个强大工具,它可以极大地简化异步任务结果的管理。通过使用CompletionService
,我们可以更容易地控制和组织异步操作,特别是在需要按序处理结果的时候。在处理大量数据同步时,正确地使用CompletionService
可以显著提高程序的效率和可维护性。记住,合理地关闭ExecutorService
是非常重要的,以确保资源的正确释放。