需要在并发场景里把线程执行顺序搞清楚?常见需求有两类:
- 汇聚型:线程 A、B 完成后再执行线程 C(A + B → C)
- 分叉型:线程 A 完成后并行执行 B、C(A → (B, C))
为什么需要显式控制线程顺序
并发不是"越并行越好"。有时任务有数据依赖或必须按阶段完成(比如先加载配置/初始化资源,再并行处理),这时需要把线程执行关系从"非确定性"变成"可控的有向依赖"。常见工具:join()、CountDownLatch、CompletableFuture,各有适用场景。
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 个预备任务,等它们都完成再干下一步 → CountDownLatch 或 CompletableFuture.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();
结语
并发控制并非只能靠"线程睡眠"或"随机等待"。把依赖关系抽象成"有向任务图",选择合适的同步原语或异步组合工具(join、CountDownLatch、CompletableFuture),既能保证正确性,也能提高可读性与可维护性。