CompletableFuture 并发统计

一、问题:串行统计太慢

很多后台系统都有"工作台/仪表盘"页面,需要一次性展示多个统计数字:在职人数、离职人数、证件到期人数、员工生日人数......

最朴素的写法是串行调用:

复制代码
long dimission = service.getDimissionTotal(param);      // 150ms
long onJob     = service.getCount(param);                // 200ms
long idCard    = service.getEmployeeIdCardExpireCount(); // 180ms
long birthday  = service.getEmployeeBirthdayCount();     // 120ms

问题 :4 次数据库查询排队执行,总耗时 = 150 + 200 + 180 + 120 = 650ms

但这 4 个查询互相独立、没有依赖关系------完全可以同时跑。


二、并发:让 4 个查询同时执行

2.1 时间线对比

复制代码
串行(650ms):
在职(200ms) → 离职(150ms) → 证件到期(180ms) → 生日(120ms)

并发(≈200ms):
在职(200ms) ┐
离职(150ms) ┤
证件到期(180ms)┼─ 同时跑,总耗时 ≈ 最慢的那个
生日(120ms) ┘

并发后总耗时从 650ms 降到约 200ms(最慢任务的耗时),提速 3 倍多。

2.2 代码实现

复制代码
EmployeeTotalVo allTotal = new EmployeeTotalVo();

CompletableFuture.allOf(
    CompletableFuture.runAsync(() -> allTotal.setDimissionTotal(
            String.valueOf(service.getDimissionTotal(param)))),
    CompletableFuture.runAsync(() -> allTotal.setBeonTheJobTotal(
            String.valueOf(service.getCount(param)))),
    CompletableFuture.runAsync(() -> allTotal.setIdcardExpireTotal(
            service.getEmployeeIdCardExpireCount(param))),
    CompletableFuture.runAsync(() -> allTotal.setStaffBirthdayTotal(
            service.getEmployeeBirthdayCount(param)))
).join();

return RequestResult.success(allTotal);

三、逐个 API 拆解

3.1 runAsync ------ 异步启动任务

复制代码
CompletableFuture.runAsync(() -> 任务代码)
  • 立即返回 一个 CompletableFuture,不阻塞当前线程;
  • 任务在独立线程 (默认 ForkJoinPool.commonPool())里执行;
  • 4 行 runAsync = 启动 4 个任务,几乎同时开始

💡 这里的 () -> ... 是 Lambda,即"这个任务具体干什么"。

3.2 allOf ------ 组合多个任务

复制代码
CompletableFuture.allOf(任务1, 任务2, 任务3, 任务4)

把多个 CompletableFuture 打包成一个"组合任务",它代表**"这几个全都完成"**这个条件。

还有个孪生兄弟:

API 放行条件
allOf(...) 全部完成
anyOf(...) 任意一个完成即可

3.3 join ------ 阻塞等待

复制代码
allOf(...).join();

阻塞当前线程 ,直到组合任务完成(即 4 个子任务都跑完)。.join() 之后的代码,必然是 4 个统计都已填入 allTotal 才会执行。

并发安全性说明 :allTotal 虽然是同一个对象,但 4 个任务设置的是不同字段 (setDimissionTotalsetBeonTheJobTotal...),互不干扰,因此并发安全。


四、join 的同类 API 全景

"等待异步任务完成"这一类,Java 提供了多种选择。理解它们的差异,才能在工程中选对。

4.1 CompletableFuture 家族

API 是否阻塞 异常处理 适用场景
join() ✅ 阻塞 CompletionException(unchecked) 业务代码首选,简洁
get() ✅ 阻塞 抛 checked 异常(必须 try-catch) 兼容老 API
getNow(默认值) ❌ 不阻塞 不阻塞 没完成就返回默认值
get(timeout, unit) ✅ 阻塞(带超时) TimeoutException 防止慢任务卡死

4.2 join vs get 的核心区别

复制代码
// join():不抛 checked 异常,代码干净
future.join();  // 直接用,无需 try-catch

// get():被迫处理 checked 异常
try {
    future.get();
} catch (InterruptedException | ExecutionException e) {
    // 被迫写 try-catch,代码啰嗦
}

结论 :业务代码优先用 join(),避免为了 checked 异常写一堆样板代码。

4.3 传统并发等待(底层/老代码)

API 特点
Future.get() ExecutorService.submit() 返回,功能类似但不能链式编排
CountDownLatch.await() 计数器到 0 才放行,适合"等 N 个线程都完成"
CyclicBarrier.await() 所有线程到齐一起继续,可复用
Thread.join() 等某个线程死亡,最底层

4.4 不阻塞的回调式编排

在响应式编程(WebFlux、Reactor)中,不能阻塞线程,改用回调链:

复制代码
future
    .thenApply(result -> 加工结果)       // 完成后转换
    .thenCompose(result -> 另一个异步)   // 完成后串联另一个异步
    .thenAccept(result -> 消费结果);     // 完成后消费

这种方式不阻塞线程,吞吐量更高,但调试更复杂。


五、工程选型建议

场景 推荐
多个独立任务并发,等全部完成 CompletableFuture.allOf(...).join() ✅(本文场景)
需要超时保护,防止慢任务卡死 .get(timeout, unit)orTimeout()(Java 9+)
老项目用线程池 submit Future.get(),但建议迁移到 CompletableFuture
高并发响应式系统 .thenApply() / .thenCompose() 回调编排
"等 N 个线程都完成"的通用同步 CountDownLatch

六、进阶:给统计接口加超时保护

生产环境有个隐患:如果某个 count 查询卡住(比如数据库锁、慢查询),.join()无限等待,整个接口超时,前端一直转圈。

改进方案------加超时:

复制代码
try {
    CompletableFuture.allOf(f1, f2, f3, f4)
            .get(3, TimeUnit.SECONDS);  // 最多等 3 秒
} catch (TimeoutException e) {
    // 超时后,已完成的字段有值,未完成的保持默认(0)
    log.warn("工作台统计部分超时,返回已完成的字段");
} catch (InterruptedException | ExecutionException e) {
    log.error("工作台统计异常", e);
}
return RequestResult.success(allTotal);

这样即使某个统计卡死,接口也能在 3 秒内返回(已完成的有值,未完成的用默认 0),保证可用性优于精确性


七、可读性优化

原代码把 4 个任务塞在 allOf 参数里,稍显拥挤。可以拆开声明,逻辑完全等价但更易读:

复制代码
CompletableFuture<Void> f1 = CompletableFuture.runAsync(
        () -> allTotal.setDimissionTotal(String.valueOf(service.getDimissionTotal(param))));
CompletableFuture<Void> f2 = CompletableFuture.runAsync(
        () -> allTotal.setBeonTheJobTotal(String.valueOf(service.getCount(param))));
CompletableFuture<Void> f3 = CompletableFuture.runAsync(
        () -> allTotal.setIdcardExpireTotal(service.getEmployeeIdCardExpireCount(param)));
CompletableFuture<Void> f4 = CompletableFuture.runAsync(
        () -> allTotal.setStaffBirthdayTotal(service.getEmployeeBirthdayCount(param)));

CompletableFuture.allOf(f1, f2, f3, f4).join();

八、总结

知识点 要点
为什么要并发 独立任务并发执行,总耗时 ≈ 最慢的任务,而非累加
三件套 runAsync(启动)→ allOf(组合)→ join(等待)
join vs get join 不抛 checked 异常,业务代码首选
allOf vs anyOf 全部完成 vs 任意一个完成
生产加固 .get(timeout) 加超时,防止单点慢任务拖垮接口
线程安全前提 并发写同一对象的不同字段才安全;写同字段需加锁或用原子类

核心心智模型 :runAsync 负责"派活",allOf 负责"收口",join 负责"等结果"。三个组合起来,就是"派多个活、同时干、干完一起收"的并发统计范式。


这套模式非常适合"工作台/仪表盘/报表汇总"等需要聚合多个独立数据源的场景。关键是判断任务之间是否真的独立 ------有依赖关系就不能简单并发,要用 thenCompose 串联。

相关推荐
ClouGence1 小时前
告别 Navicat!CloudDM:开源免费一站式数据库开发管理工具
数据库·sql·开源·数据库开发
灵析表格1 小时前
灵析表格功能函数深度分析报告
前端·数据库·microsoft
jaboo121 小时前
postgresql从入门到精通
数据库·postgresql·langchain
倔强的小石头_1 小时前
中兴F50只能本地管理?刷入UFI-TOOLS后这样远程访问
网络
晴天¥1 小时前
记一次Oracle集群归档日志爆满之后如何处理(涉及ASM磁盘的增添。有坑必踩)
数据库·oracle
云边有个稻草人1 小时前
Ubuntu部署Prometheus与Alertmanager:服务接入和远程监控
数据库·ubuntu·prometheus
Hoxy.R1 小时前
记录一次Oracle 19c RAC iSCSI会话反复重连:comm error (1020) 原因定位与解决
数据库·oracle
@CLoudbays_Martin111 小时前
Linux 服务器如何查看是否被植入后门?
linux·服务器·网络·安全·github·ssl
拾光Ծ2 小时前
【Linux网络】深入理解网络层:从IP协议格式,子网划分到NAT与路由机制
linux·网络·网络协议·tcp/ip·计算机网络