第19题:多线程如何顺序执行?
📚 回答:
- 核心考点: 多线程顺序执行不是"让线程排队"这么简单,大厂面试不会只问"用 join 还是 CountDownLatch",而是深入考察 不同方案的本质差异(线程级顺序 vs 任务级顺序 vs 数据级顺序)、以及高并发场景下的性能与可扩展性权衡(单线程池的串行瓶颈、CountDownLatch 的级联唤醒机制、CompletableFuture 的异步编排)。面试官真正想判断的是:你是否能根据业务场景(顺序的严格程度、线程数量、是否需要返回值)选择最优方案。
1. 方案一:join()——线程级的顺序等待
-
1.1 原理与实现
join()让调用线程(通常是主线程)等待目标线程执行完毕:Thread t1 = new Thread(() -> System.out.println("A")); Thread t2 = new Thread(() -> System.out.println("B")); Thread t3 = new Thread(() -> System.out.println("C")); t1.start(); t1.join(); // 主线程 WAITING,等待 t1 TERMINATED t2.start(); t2.join(); // 主线程 WAITING,等待 t2 TERMINATED t3.start(); t3.join(); // 主线程 WAITING,等待 t3 TERMINATED -
1.2 底层源码
// Thread.join() 源码 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) throw new IllegalArgumentException("timeout value is negative"); if (millis == 0) { while (isAlive()) { // 目标线程是否存活 wait(0); // 当前线程 WAITING,等待目标线程结束 } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) break; wait(delay); // 限时等待 now = System.currentTimeMillis() - base; } } }关键机制:目标线程结束时,JVM 自动调用
this.notifyAll()唤醒所有等待该线程的线程。 -
1.3 优缺点
维度 评价 优点 简单直观,无需额外工具类 缺点 线程必须 TERMINATED 才能继续,无法复用;主线程阻塞,浪费资源;无法处理异常和返回值 适用 简单脚本、单元测试、少量线程
2. 方案二:单线程线程池——任务级的顺序执行
-
2.1 原理与实现
Executors.newSingleThreadExecutor()内部使用LinkedBlockingQueue,所有任务按提交顺序串行执行:ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(() -> System.out.println("A")); executor.submit(() -> System.out.println("B")); executor.submit(() -> System.out.println("C")); executor.shutdown(); -
2.2 底层结构
// newSingleThreadExecutor 的构造 public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService( new ThreadPoolExecutor(1, 1, // 核心线程数=1,最大线程数=1 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); // 无界队列 }顺序保障:只有一个工作线程,任务从队列中 FIFO 取出执行。
-
2.3 优缺点
维度 评价 优点 线程复用,任务顺序执行;支持 Future 返回值;支持异常处理 缺点 单线程串行,吞吐量低;队列无界可能 OOM;线程异常终止后需重建 适用 顺序任务队列、日志写入、消息顺序消费 -
2.4 生产级改进
// 使用有界队列 + 拒绝策略,防止 OOM ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1000), // 有界队列 new ThreadFactoryBuilder().setNameFormat("ordered-pool-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时主线程执行 );
3. 方案三:CountDownLatch——事件驱动的顺序控制
-
3.1 原理与实现
通过计数器控制线程间的依赖关系:CountDownLatch latchA = new CountDownLatch(1); CountDownLatch latchB = new CountDownLatch(1); Thread t1 = new Thread(() -> { System.out.println("A"); latchA.countDown(); // 信号:A 完成 }); Thread t2 = new Thread(() -> { latchA.await(); // 等待 A 完成 System.out.println("B"); latchB.countDown(); // 信号:B 完成 }); Thread t3 = new Thread(() -> { latchB.await(); // 等待 B 完成 System.out.println("C"); }); t1.start(); t2.start(); t3.start(); -
3.2 优缺点
维度 评价 优点 线程可并行启动,按需等待;支持多对多依赖;可复用线程 缺点 代码复杂,Latch 数量随线程增加;无法获取返回值;一次性使用 适用 流水线任务、多阶段并行计算 -
3.3 与 join() 的本质区别
特性 join() CountDownLatch 等待对象 目标线程结束 事件信号(计数器归零) 线程复用 ❌ 线程必须结束 ✅ 线程可继续执行其他任务 多对多 ❌ 一对一 ✅ 多线程等待同一事件 返回值 ❌ 无 ❌ 无
4. 方案四:CompletableFuture——异步编排的现代方案
-
4.1 顺序执行
CompletableFuture<Void> future = CompletableFuture .runAsync(() -> System.out.println("A")) .thenRun(() -> System.out.println("B")) .thenRun(() -> System.out.println("C")); future.join(); // 等待全部完成 -
4.2 带返回值的顺序执行
CompletableFuture<String> result = CompletableFuture .supplyAsync(() -> { // 步骤1:获取用户 System.out.println("查询用户"); return "User:张三"; }) .thenApply(user -> { // 步骤2:依赖步骤1结果 System.out.println("查询订单: " + user); return user + ", Orders:3"; }) .thenApply(orderInfo -> { // 步骤3:依赖步骤2结果 System.out.println("计算积分: " + orderInfo); return orderInfo + ", Points:300"; }); System.out.println(result.join()); // User:张三, Orders:3, Points:300 -
4.3 异常处理
CompletableFuture<String> future = CompletableFuture .supplyAsync(() -> { if (true) throw new RuntimeException("步骤1失败"); return "A"; }) .exceptionally(ex -> { System.out.println("异常捕获: " + ex.getMessage()); return "默认值"; }) .thenApply(result -> result + "→B"); -
4.4 优缺点
维度 评价 优点 函数式编程,链式调用;支持返回值和异常处理;可组合并行/串行;非阻塞(主线程不等待) 缺点 学习曲线陡峭;调试困难(堆栈不直观);默认使用 ForkJoinPool,线程数有限 适用 微服务编排、异步流水线、复杂依赖图
5. 方案五:Semaphore / CyclicBarrier——多线程协同的顺序控制
-
5.1 Semaphore 控制并发数
Semaphore semaphore = new Semaphore(1); // 只允许1个线程执行 for (int i = 0; i < 3; i++) { final int index = i; new Thread(() -> { try { semaphore.acquire(); // 获取许可,只有一个线程能通过 System.out.println("Task " + index); Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); // 释放许可 } }).start(); } -
5.2 CyclicBarrier 循环屏障
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("=== 阶段完成 ===")); for (int i = 0; i < 3; i++) { final int index = i; new Thread(() -> { try { System.out.println("Task " + index + " 阶段1"); barrier.await(); // 等待所有线程到达 System.out.println("Task " + index + " 阶段2"); barrier.await(); // 再次等待 } catch (Exception e) { e.printStackTrace(); } }).start(); }
6. 方案六:volatile + 状态变量——轻量级顺序控制
- 6.1 实现
注意:纯自旋浪费 CPU,生产环境应使用public class VolatileOrder { private volatile int state = 0; public void taskA() { System.out.println("A"); state = 1; // 信号:A 完成 } public void taskB() { while (state != 1) { /* 自旋等待 */ } // 实际应配合 LockSupport System.out.println("B"); state = 2; } public void taskC() { while (state != 2) { /* 自旋等待 */ } System.out.println("C"); } }LockSupport.park/unpark或Condition。
7. 全方案对比与选型
| 方案 | 顺序粒度 | 线程复用 | 返回值 | 异常处理 | 吞吐量 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|---|---|
| join() | 线程级 | ❌ | ❌ | ❌ | 低 | 低 | 简单脚本、测试 |
| 单线程池 | 任务级 | ✅ | ✅ | ✅ | 低 | 低 | 顺序队列、日志 |
| CountDownLatch | 事件级 | ✅ | ❌ | ❌ | 中 | 中 | 流水线、多阶段 |
| CompletableFuture | 函数级 | ✅ | ✅ | ✅ | 高 | 高 | 微服务编排 |
| Semaphore | 许可级 | ✅ | ❌ | ❌ | 中 | 低 | 限流顺序执行 |
| CyclicBarrier | 阶段级 | ✅ | ❌ | ❌ | 中 | 中 | 分阶段并行 |
| volatile 状态 | 变量级 | ✅ | ❌ | ❌ | 高 | 低 | 极简单场景 |
8. 生产环境避坑指南
-
8.1 join() 的陷阱
// ❌ 错误:先 start 再 join,无法并行 t1.start(); t1.join(); t2.start(); t2.join(); // t2 等 t1 结束后才启动,没有并行度 // ✅ 正确:先全部 start,再依次 join t1.start(); t2.start(); t3.start(); t1.join(); t2.join(); t3.join(); // 并行执行,最后等待 -
8.2 CountDownLatch 的计数器不可重置
// CountDownLatch 是一次性的! CountDownLatch latch = new CountDownLatch(1); latch.countDown(); latch.await(); // 立即通过 // 不能再次使用!如需循环使用,改用 CyclicBarrier -
8.3 CompletableFuture 的线程池陷阱
// ❌ 错误:默认 ForkJoinPool 线程数 = CPU 核心数,IO 密集型会阻塞 CompletableFuture.runAsync(() -> { // 执行 HTTP 请求,阻塞 ForkJoin 线程 }); // ✅ 正确:自定义线程池 ExecutorService executor = Executors.newFixedThreadPool(20); CompletableFuture.runAsync(() -> { // IO 操作 }, executor); -
8.4 单线程池的异常处理
ExecutorService executor = Executors.newSingleThreadExecutor(); Future<?> future = executor.submit(() -> { throw new RuntimeException("任务异常"); }); try { future.get(); // 必须调用 get() 才能感知异常! } catch (ExecutionException e) { System.out.println("捕获异常: " + e.getCause().getMessage()); }
9. 面试官追问与高分回答模板
-
追问 1:“多线程如何顺序执行?”
- 低分回答:“用 join、单线程线程池或 CountDownLatch。”(没有对比和选型)
- 高分回答:
"多线程顺序执行有六种主流方案,选型取决于顺序粒度和业务需求:
- join():线程级顺序,简单但线程不可复用,适合测试和脚本;
- 单线程线程池:任务级顺序,线程复用,适合日志写入、消息顺序消费;
- CountDownLatch:事件级顺序,线程可并行启动按需等待,适合流水线;
- CompletableFuture:函数级顺序,支持链式调用、返回值、异常处理,适合微服务编排;
- Semaphore:许可级顺序,控制并发数为 1,适合限流场景;
- CyclicBarrier:阶段级顺序,多线程分阶段协同,适合 MapReduce。
现代 Java 项目优先推荐 CompletableFuture,它兼顾了顺序控制、异步非阻塞、异常处理和返回值。"
-
追问 2:“join() 和 CountDownLatch 有什么区别?”
- 高分回答:
"核心区别在于等待的对象和线程复用性:
- 等待对象:
join()等待目标线程 TERMINATED;CountDownLatch等待计数器归零(事件信号); - 线程复用:
join()要求线程结束,不可复用;CountDownLatch线程只需 countDown,可继续执行其他任务; - 多对多:
join()只能一对一等待;CountDownLatch支持多线程等待同一事件(如 10 个线程等 3 个初始化完成); - 返回值:两者都不支持返回值,但
CountDownLatch可配合Callable+Future实现。
类比:
join()像等朋友吃完饭(人必须离开);CountDownLatch像等朋友发微信说’我吃完了’(人还可以继续逛街)。" - 等待对象:
- 高分回答:
-
追问 3:“CompletableFuture 的 thenRun 和 thenApply 有什么区别?”
- 高分回答:
"两者都是顺序执行,但语义不同:
- thenRun(Runnable):不接收上游结果,无返回值。用于’执行完 A 后执行 B,B 不需要 A 的结果’;
- thenApply(Function):接收上游结果,有返回值。用于’执行完 A 后用 A 的结果执行 B’;
- thenAccept(Consumer):接收上游结果,无返回值。用于’执行完 A 后用 A 的结果做消费’。
示例:
supplyAsync(() -> 'A') // 返回 'A' .thenRun(() -> print('B')) // 不接收 'A',打印 'B' .thenApply(s -> s + 'C') // 接收 'A',返回 'AC' .thenAccept(s -> print(s)); // 接收 'AC',消费打印另外,
thenRun/thenApply使用同一线程执行(如果上游已完成);thenRunAsync/thenApplyAsync使用新线程执行。"
- 高分回答:
-
追问 4:“单线程线程池和 CountDownLatch 怎么选?”
- 高分回答:
"取决于任务特征:
- 单线程线程池:任务是同质且持续产生的(如日志队列、消息消费),顺序是队列的固有属性,无需额外协调;
- CountDownLatch:任务是异质且有明确依赖关系的(如 A→B→C 流水线),需要显式的事件信号控制顺序。
另外,单线程线程池的吞吐量受限于单线程,如果任务中有 IO 操作,整个队列都会阻塞。此时应考虑:
- 将 IO 操作异步化;
- 使用 CompletableFuture 的 thenCompose 编排异步任务。"
- 高分回答:
-
追问 5:“如果要求 10 个线程分 3 个阶段顺序执行,每个阶段内并行,阶段间串行,怎么实现?”
- 高分回答:
"这是 CyclicBarrier 的经典场景:
CyclicBarrier barrier = new CyclicBarrier(10, () -> System.out.println('阶段完成')); for (int i = 0; i < 10; i++) { new Thread(() -> { try { // 阶段1:并行执行 doPhase1(); barrier.await(); // 等全部 10 个线程完成阶段1 // 阶段2:并行执行 doPhase2(); barrier.await(); // 等全部完成阶段2 // 阶段3:并行执行 doPhase3(); } catch (Exception e) { ... } }).start(); }CyclicBarrier 的
await()会阻塞线程直到计数器归零,然后执行可选的 Runnable 回调,最后重置计数器供下一轮使用。与 CountDownLatch 的区别:CyclicBarrier 可循环使用,CountDownLatch 一次性。"
- 高分回答:
-
追问 6:“CompletableFuture 怎么实现并行执行、全部完成后汇总?”
- 高分回答:
"使用
allOf或anyOf组合多个 CompletableFuture:CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> fetchUser()); CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> fetchOrder()); CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> fetchPoints()); // 全部完成后汇总 CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3); all.thenRun(() -> { String user = f1.join(); // 此时已确保完成,不会阻塞 String order = f2.join(); String points = f3.join(); System.out.println(user + order + points); }); // 任意一个完成即继续 CompletableFuture<Object> any = CompletableFuture.anyOf(f1, f2, f3); any.thenAccept(result -> System.out.println('最先完成: ' + result));注意:
allOf返回的 CompletableFuture 类型是Void,需通过原始 Future 的join()获取结果。"
- 高分回答:
10. 方案选型速查表
| 业务场景 | 推荐方案 | 核心理由 |
|---|---|---|
| 简单脚本/测试 | join() | 简单直观,无需工具类 |
| 日志顺序写入 | 单线程线程池 | 任务队列 FIFO,线程复用 |
| 消息顺序消费 | 单线程线程池 + 分区 | 按 Key 分区,每区单线程 |
| 流水线 A→B→C | CountDownLatch | 事件驱动,线程可复用 |
| 微服务编排 | CompletableFuture | 链式调用,支持异常和返回值 |
| 多阶段并行计算 | CyclicBarrier | 阶段同步,可循环使用 |
| 限流顺序执行 | Semaphore(1) | 控制并发数为 1 |
| 复杂依赖图 | CompletableFuture + allOf | 灵活组合并行/串行 |
💡 面试官想要的满分总结:
多线程顺序执行不是"让线程排队",而是控制执行流的依赖关系。现代 Java 开发中,
CompletableFuture是首选方案——它用函数式编程替代了传统的线程管理,支持链式顺序编排、并行组合、异常处理和返回值,完美适配微服务架构。但经典方案仍有其价值:
join()适合简单测试;单线程线程池适合顺序任务队列;CountDownLatch适合流水线事件驱动;CyclicBarrier适合分阶段并行计算。选型核心:任务同质用队列,任务异质用编排,阶段协同用屏障。生产避坑:单线程池用有界队列防 OOM;CompletableFuture 自定义线程池防 ForkJoin 阻塞;CountDownLatch 一次性不可复用;join() 先全部 start 再依次 join 才能并行。
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

被折叠的 条评论
为什么被折叠?



