关于 Java 中的线程顺序控制:三种实战方案(join / CountDownLatch / CompletableFuture)

需要在并发场景里把线程执行顺序搞清楚?常见需求有两类:

  • 汇聚型:线程 A、B 完成后再执行线程 C(A + B → C)
  • 分叉型:线程 A 完成后并行执行 B、C(A → (B, C))

为什么需要显式控制线程顺序

并发不是"越并行越好"。有时任务有数据依赖或必须按阶段完成(比如先加载配置/初始化资源,再并行处理),这时需要把线程执行关系从"非确定性"变成"可控的有向依赖"。常见工具:join()CountDownLatchCompletableFuture,各有适用场景。


1. 使用 join() ------ 主线程驱动、直观但手工

适用场景:线程数量少、主线程可以承担协调责任的简单场景。

汇聚型:A 与 B 并行,等它们都结束再启动 C

ini 复制代码
Thread a = new Thread(() -> { doWork("A"); });
Thread b = new Thread(() -> { doWork("B"); });

a.start();
b.start();

a.join();
b.join();

Thread c = new Thread(() -> { doWork("C"); });
c.start();
c.join();

分叉型:先执行 A,A 结束后并行执行 B、C

ini 复制代码
Thread a = new Thread(() -> { doWork("A"); });
a.start();
a.join();

Thread b = new Thread(() -> { doWork("B"); });
Thread c = new Thread(() -> { doWork("C"); });
b.start();
c.start();
b.join();
c.join();

优点 :简单、可读性强。
缺点 :需要主线程(或控制线程)管理 join 顺序,不适合复杂 DAG;长时间阻塞主线程可能不理想;要显式处理 InterruptedException


2. 使用 CountDownLatch ------ 典型的"等待多个完成再继续"

适用场景:多个前置任务完成后触发后续任务;协调者和参与者分离的场景。

汇聚型(A + B → C)

scss 复制代码
CountDownLatch latch = new CountDownLatch(2);

new Thread(() -> { doWork("A"); latch.countDown(); }).start();
new Thread(() -> { doWork("B"); latch.countDown(); }).start();

new Thread(() -> {
    latch.await(); // 等 A 与 B 都完成
    doWork("C");
}).start();

分叉型(A → (B, C))

scss 复制代码
CountDownLatch latch = new CountDownLatch(1);

new Thread(() -> { doWork("A"); latch.countDown(); }).start();

new Thread(() -> { latch.await(); doWork("B"); }).start();
new Thread(() -> { latch.await(); doWork("C"); }).start();

优点 :语义明确,参与者无需主线程 join;CountDownLatch 用法简单,并发控制清晰。
缺点 :一次性(count 到 0 后不可重用);需要手动管理每个参与方的 countDown() 调用;要处理中断。


3. 使用 CompletableFuture ------ 现代、函数式、易组合(推荐用于任务编排)

适用场景:任务链、复杂依赖、多任务组合与后续处理,想要非阻塞式、声明式控制时。

汇聚型(A + B → C)

scss 复制代码
CompletableFuture<Void> fA = CompletableFuture.runAsync(() -> doWork("A"));
CompletableFuture<Void> fB = CompletableFuture.runAsync(() -> doWork("B"));

CompletableFuture.allOf(fA, fB)
    .thenRun(() -> doWork("C"))
    .join(); // 等待最终完成(可选)

分叉型(A → (B, C))

scss 复制代码
CompletableFuture<Void> fA = CompletableFuture.runAsync(() -> doWork("A"));

fA.thenRunAsync(() -> doWork("B"));
fA.thenRunAsync(() -> doWork("C"));

fA.join(); // 等 A 完成后自动触发 B 和 C(如果需要等待全部完成,可以 join allOf)

要等所有完成再退出:

ini 复制代码
CompletableFuture<Void> fB = fA.thenRunAsync(() -> doWork("B"));
CompletableFuture<Void> fC = fA.thenRunAsync(() -> doWork("C"));
CompletableFuture.allOf(fB, fC).join();

优点 :非常适合构造复杂依赖图(DAG);非阻塞、链式 API、易扩展;支持异常处理、超时、组合等丰富功能。
缺点 :理解曲线比 join/CountDownLatch 稍陡;默认使用 ForkJoinPool.commonPool(),需要注意线程池与阻塞任务的搭配(阻塞任务建议提供自定义线程池)。


4. 实战注意事项(面试与生产都要知道)

  • 处理中断join()await() 都会抛 InterruptedException,不要吞掉异常,合理中断处理或恢复中断。
  • 异常传播 :子线程抛异常不会自动抛到等待线程(join() 不会抛子线程异常);使用 CompletableFuture 能更好地捕获/组合异常。
  • 阻塞与线程池 :用 CompletableFuture.runAsync 时,如果任务会阻塞(IO/锁),别用 commonPool,传入自定义 Executor
  • 资源与复用CountDownLatch 一次性;如果需要重复使用可考虑 CyclicBarrier 或自己重置的方案。
  • 死锁风险 :在回调中再 join() 恶化顺序可能导致死锁;设计依赖图时要确保无环。
  • 可观测性:在生产代码里加足够的日志/监控,方便定位哪个任务阻塞或失败。

5. 何时用哪种方案(速查表)

  • 只想简单"串行化"或主线程控制 → join()
  • 有 N 个预备任务,等它们都完成再干下一步 → CountDownLatchCompletableFuture.allOf
  • 想写可组合、可扩展、带异常处理的异步流程 → CompletableFuture
  • 需要可重用的栅栏(多轮同步) → CyclicBarrier
  • 高并发统计/通知场景 → 考虑更高层次的框架(例如基于消息或任务队列)。

6. 最后给出一个通用模板(伪代码,可直接复制改造)

scss 复制代码
// 模板:A 和 B 并行完成后再执行 C
CompletableFuture<Void> fa = CompletableFuture.runAsync(() -> task("A"), executor);
CompletableFuture<Void> fb = CompletableFuture.runAsync(() -> task("B"), executor);

CompletableFuture.allOf(fa, fb)
    .thenRunAsync(() -> task("C"), executor)
    .exceptionally(ex -> { log(ex); return null; })
    .join();

结语

并发控制并非只能靠"线程睡眠"或"随机等待"。把依赖关系抽象成"有向任务图",选择合适的同步原语或异步组合工具(joinCountDownLatchCompletableFuture),既能保证正确性,也能提高可读性与可维护性。

相关推荐
洋洋技术笔记2 小时前
Spring Boot启动流程解析
spring boot·后端
mudtools2 小时前
搭建一套.net下能落地的飞书考勤系统
后端·c#·.net
用户7344028193422 小时前
在Spring Boot项目中引入本地JAR包的步骤和配置
后端
武子康2 小时前
大数据-235 离线数仓 - 实战:Flume+HDFS+Hive 搭建 ODS/DWD/DWS/ADS 会员分析链路
大数据·后端·apache hive
Moment2 小时前
一周重写 Next.js?Cloudflare 和 AI 做到了😍😍😍
前端·javascript·后端
CodeSheep3 小时前
同事去年绩效是C,提离职领导死活不让走,后来领导私下说他走了,就没人背这个绩效了
前端·后端·程序员
怕浪猫3 小时前
第19章:Go语言工具链与工程实践
后端·go·编程语言
BingoGo3 小时前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·laravel
风象南3 小时前
WHAT? AI把我仓库内容全删了!!!
后端