线程协同:T3 如何优雅地等到 T1/T2 执行完?
面试高频问题:
T1、T2 并行执行,要求 T3 必须在 T1、T2 都完成后再执行。
本质是考你:多线程协同机制 + JUC 工具类 + Future/FutureTask/线程池的理解。
先场景抽象理解,再自己具象出场景答题(做到场景可控)
先把场景抽象出来:
“等三个都跑完再继续”的核心就是:
把每个子任务包装成一个可等待的句柄(handle),然后在主线程上统一等待这些句柄完成。
对应不同的方法,再用一个简单的,可变化的通用 实际项目场景 具象化串起来。
比如:
项目实例:异步首页查询对应模块任务卡片数量
- 模块一:查询“待办数”
- 模块二:查询“入库异常数”
- 模块三:查询“出库异常数”
要求:三个查询并行,都查完后再组装首页数据返回。
一、理解线程协同的不同需求,做到心中有层级
-
先区分“线程顺序”还是“任务顺序”
- 有的场景关注的是:
T1 → T2 → T3这个线程先后,比如多线程交替打印 ABC; - 更多真实业务,其实只关心“任务 A、任务 B、任务 C 的执行顺序”,并不在意具体是哪个线程跑的。
- 这会直接影响你是用
join/Condition这类“线程视角”的工具,还是用单线程池 /CompletableFuture这种“任务视角”的工具。
- 有的场景关注的是:
-
再看要不要结果、要不要组合结果
- 如果只是“按顺序做几件事”,不关心返回值(例如按顺序打日志、发通知),可以用最简单的协作手段;
- 如果要并发执行 + 汇总多个返回结果(例如首页并发查多个模块卡片数量),就需要
ExecutorService + Future或CompletableFuture.allOf这种“带结果的协作方案”。
-
判断是“一次性顺序”还是“多阶段协同”
- 有的场景只需要:一批任务都执行完,再继续下一步,典型代表是:
join、CountDownLatch、allOf; - 有的场景是:要分“阶段一 / 阶段二 / 阶段三”,每一阶段内部并行,阶段之间要同步,这时候就要考虑
CyclicBarrier、Phaser一类的“分阶段协同工具”。
- 有的场景只需要:一批任务都执行完,再继续下一步,典型代表是:
-
最后考虑复杂度:简单业务用简单工具,复杂协作再上锁/条件队列
- 如果只是简单顺序控制,
join、单线程池、CountDownLatch、CompletableFuture就足够了; - 只有在“需要精确点名唤醒谁”“多线程交替打印”“复杂流水线”的少数场景下,才有必要上
Semaphore、ReentrantLock+Condition这种更底层的控制手段。
- 如果只是简单顺序控制,
把这四个维度讲完后,再顺着说:
- 基础顺序控制:
join、单线程池; - 一次性等待一批任务:
CountDownLatch、CompletableFuture.allOf、ExecutorService + Future/FutureTask; - 多阶段协同:
CyclicBarrier、Phaser; - 精细控制先后:
Semaphore、ReentrantLock+Condition、BlockingQueue;
二、 三类-- 9 种方案
一类:基础顺序控制(最简单,面向“线程顺序”)
1. Thread.join() —— 直接等线程执行完(入门必会)
是什么
- 当前线程在
join()上阻塞,一直到目标线程执行完。
怎么实现 T1 → T2 → T3
Thread t1 = new Thread(() -> System.out.println("T1"));
Thread t2 = new Thread(() -> System.out.println("T2"));
Thread t3 = new Thread(() -> System.out.println("T3"));
t1.start();
t1.join(); // 等 T1
t2.start();
t2.join(); // 等 T2
t3.start();
t3.join(); // 等 T3
优点
- API 极简,语义直观,适合面试第一层回答。
缺点
- 一对一等待,不好扩展到复杂依赖;
- 不自带结果聚合和异常链;
- 代码容易写成“join 地狱”。
适用场景
- demo、练习题、非常简单的顺序控制。
2. 单线程线程池 newSingleThreadExecutor() —— 把“线程问题”变成“排队问题”
是什么
- 线程池只有 1 条工作线程,所有任务排队串行执行,天然保证顺序。
简单示例
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("任务 A"));
executor.execute(() -> System.out.println("任务 B"));
executor.execute(() -> System.out.println("任务 C"));
executor.shutdown();
执行顺序:A → B → C。
优点
- 不用管锁、不用管信号量,“你先提交谁,就先执行谁”;
- 复用线程池的生命周期管理、拒绝策略等。
缺点
- 吞吐量只有一条线程;
- 只解决“任务顺序”,不解决多阶段协同。
适用场景
- 某一段逻辑必须严格串行,但整体系统仍是多线程。
二类:同步点/阶段协作(面向“等谁都到了再走”)
3. CountDownLatch —— 等 N 个任务都完成再继续(最常用协作工具)
是什么
- 一个初始化为 N 的计数器,任务完成一次
countDown(),等待方在await()处等到 0。
T3 等 T1/T2
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
try {
System.out.println("T1");
} finally {
latch.countDown();
}
}).start();
new Thread(() -> {
try {
System.out.println("T2");
} finally {
latch.countDown();
}
}).start();
latch.await(); // 等 T1/T2 都完
System.out.println("T3");
优点
- 语义清晰:“我要等 N 个任务完成”;
- 与具体线程解耦(只关注“事件”完没完);
- 实现简单,JUC 常用。
缺点
- 一次性使用,不能重置;
- 不带返回值和异常链,需要你自己维护结果容器。
适用场景
- “等 3 个接口都查完再合并返回”
- “等多个模块初始化后再启动主服务”。
4. CyclicBarrier —— 多线程在同一阶段“到齐再走”
是什么
- 一批线程都在
await()这里等; - 当参与者数量达到设定值,栅栏打开,所有线程一起进入下一阶段;
- 可以重复使用多轮。
简单示例
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties,
() -> System.out.println("阶段一结束,所有线程一起进入阶段二")
);
Runnable task = () -> {
try {
System.out.println(Thread.currentThread().getName() + " 阶段一");
barrier.await();
System.out.println(Thread.currentThread().getName() + " 阶段二");
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(task, "T1").start();
new Thread(task, "T2").start();
new Thread(task, "T3").start();
优点
- 支持多轮阶段同步;
- 可设置“栅栏打开时的回调”,做阶段收尾逻辑。
缺点
- 只控制“所有人到齐再走”,不控制“谁先谁后”;
- 出错线程如果没抵达栅栏,其他线程会一直卡在
await()。
适用场景
- 多线程并行计算 + 每轮需要所有线程都算完再进入下一轮。
5. Phaser —— 多阶段 + 可动态增减参与者
是什么
- 可以看作“升级版 CyclicBarrier”;
- 支持多阶段(phase 0、1、2…),每个阶段都
arriveAndAwaitAdvance(); - 支持动态注册/注销参与线程。
示例
Phaser phaser = new Phaser(3); // 三个参与线程
Runnable task = () -> {
for (int phase = 0; phase < 3; phase++) {
System.out.println(Thread.currentThread().getName() + " 执行阶段 " + phase);
phaser.arriveAndAwaitAdvance(); // 等其他线程到达本阶段
}
};
new Thread(task, "T1").start();
new Thread(task, "T2").start();
new Thread(task, "T3").start();
优点
- 适合“多阶段 + 参与线程数量动态变化”的复杂流程;
- 比 CyclicBarrier 更灵活。
缺点
- API 和用法复杂度更高,小场景不划算;
- 面试中提到即可,一般不用详细展开。
适用场景
- 多轮 Map-Reduce 类任务;
- 复杂流水线,多阶段之间需要严格同步。
三类:信号/条件控制(面向“精准谁先谁后”)
6. Semaphore —— 用“令牌”显式串起顺序
是什么
- 内部持有 N 个“许可”;
acquire()拿不到许可就阻塞;release()归还许可。
实现 T1 → T2 → T3
Semaphore s1 = new Semaphore(0);
Semaphore s2 = new Semaphore(0);
Thread t1 = new Thread(() -> {
System.out.println("T1");
s1.release(); // 放行 T2
});
Thread t2 = new Thread(() -> {
try {
s1.acquire(); // 等 T1
System.out.println("T2");
s2.release(); // 放行 T3
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t3 = new Thread(() -> {
try {
s2.acquire(); // 等 T2
System.out.println("T3");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t3.start();
t2.start();
t1.start();
优点
- 灵活,可实现多种“谁等谁”的顺序;
- 也常用来做并发限流(限制最大并发数)。
缺点
- 可读性一般,“令牌流向”一旦写乱就难调;
- 不适合特别复杂的大量线程协作。
适用场景
- 少量线程,明确先后依赖;
- 一边限流一边保证先后(如打印顺序控制 + 最大并发)。
7. ReentrantLock + Condition —— 最精细的“点名唤醒”
是什么
ReentrantLock:可重入显式锁;Condition:挂在锁上的条件队列;- 线程在
condition.await()上挂起,另一个线程在持有同一把锁时调用condition.signal()唤醒。
实现 T1 → T2 → T3(简化版)
ReentrantLock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
final int[] state = {1}; // 1->T1, 2->T2, 3->T3
后面每个线程:
- 在对应的 Condition 上
await(),只有轮到自己时才被唤醒; - 自己执行完后改
state,并signal下一个的 Condition。
优点
- 精确控制唤醒顺序;
- 能实现非常复杂的“线程交替打印”“多线程轮流干活”等模式。
缺点
- 代码啰嗦,对锁控制要求高,一不小心就死锁 / 丢信号;
- 不推荐在简单业务里上来就用。
适用场景
- 高频面试题:多个线程交替打印 ABC;
- 复杂协作框架内部,而不是普通业务代码。
四类:任务/结果编排 & 消息传递(面向“任务级顺序 + 结果”)
8. BlockingQueue —— 用“队列 + 令牌”自然表达顺序
是什么
- 阻塞队列:FIFO,
put()满时阻塞、take()空时阻塞; - 常见类型:
ArrayBlockingQueue、LinkedBlockingQueue。
用两个队列实现 T1 → T2 → T3
BlockingQueue<Integer> q1 = new ArrayBlockingQueue<>(1);
BlockingQueue<Integer> q2 = new ArrayBlockingQueue<>(1);
Thread t1 = new Thread(() -> {
try {
System.out.println("T1");
q1.put(1); // 通知 T2
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t2 = new Thread(() -> {
try {
q1.take(); // 等 T1
System.out.println("T2");
q2.put(1); // 通知 T3
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread t3 = new Thread(() -> {
try {
q2.take(); // 等 T2
System.out.println("T3");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t3.start();
t2.start();
t1.start();
优点
- 思路直观,和生产者/消费者模型天然契合;
- 顺序是“令牌流经队列”的顺序,很好理解。
缺点
- 只适合“少数参与方 + 简单控制链”,太多队列会乱;
- 粒度偏粗,没法像 Condition 那样细到“某种条件”。
适用场景
- 分阶段流水线,一个阶段的输出是下个阶段的输入;
- 简单顺序控制 + 模拟消息传递。
9. CompletableFuture(配合线程池/FutureTask)——现代项目里真正“该用的”
9.1 业务视角:首页异步查询示例
public Map<String, Integer> getFirstPageCount_Common(FirstPageListDto condition) {
// 线程池可以是业务公共线程池
ExecutorService executor = ...;
CompletableFuture<Integer> reqFuture = CompletableFuture.supplyAsync(
() -> queryReqCount(condition), executor);
CompletableFuture<Integer> inFuture = CompletableFuture.supplyAsync(
() -> queryInCount(condition), executor);
CompletableFuture<Integer> outFuture = CompletableFuture.supplyAsync(
() -> queryOutCount(condition), executor);
// 等 3 个都执行完
CompletableFuture.allOf(reqFuture, inFuture, outFuture).join();
Map<String, Integer> result = new HashMap<>();
result.put("reqCount", reqFuture.join());
result.put("inCount", inFuture.join());
result.put("outCount", outFuture.join());
return result;
}
这里 allOf(...).join() 就是 “等 T1/T2/T3 都完成再往下继续”。
9.2 CompletableFuture 是什么?和 Future 什么关系?
public class CompletableFuture<T>
implements Future<T>, CompletionStage<T> {
// 既是 Future(可 get),又是 CompletionStage(可链式 thenXxx)
}
和传统 Future 的对比:
| 能力 | Future | CompletableFuture |
|---|---|---|
| 有无返回值 | ✅ | ✅ |
| 获取结果方式 | get() 阻塞 | get()/join() + 回调 + 链式处理 |
| 多任务组合 | 手动管理列表 | allOf/anyOf/thenCombine/thenCompose |
| 异常处理 | ExecutionException | exceptionally/handle/whenComplete |
CompletableFuture 本质就是:增强版 Future + 完整的异步流水线语义。
9.3 allOf 的核心原理
CompletableFuture.runAsync/supplyAsync把任务丢进线程池,返回一个CompletableFuture对象,这个对象就是“任务句柄”。CompletableFuture.allOf(f1,f2,f3)再返回一个新的CompletableFuture<Void>:- 内部维护一个计数器 = N(future 个数);
- 每个子 Future 完成时计数器减 1;
- 减到 0 时,把这个 allOf Future 标记完成;
- 主线程在 allOf 上
.join()/.get()就实现了:等所有子任务都结束再继续。
9.4 链式组合、异常处理一并搞定
比如首页场景需要:
- 先并发查 3 个模块;
- 再汇总结果做个埋点日志;
- 期间要做好异常兜底。
可以:
CompletableFuture<Void> allFuture = CompletableFuture.allOf(reqFuture, inFuture, outFuture);
allFuture
.thenRun(() -> logHomeCardCount(reqFuture.join(), inFuture.join(), outFuture.join()))
.exceptionally(ex -> {
logError("首页卡片查询异常", ex);
return null;
})
.join();
重点:
- 等待 + 汇总 + 异常处理都在一个链上,代码语义非常清晰,面试时可当“现代写法”重点讲。
适用场景
- 多任务并发 + 需要结果 + 需要组合 / 聚合 + 要处理异常;
- 例如:首页多个卡片数量并发查询、多个下游服务并发调用再合并返回。
优点
- 一行
allOf就能表达“等这些都完成”; thenXxx链式编排语义清晰;- 异常处理可统一挂在链上;
- 和线程池 / FutureTask 体系天然兼容。
缺点
- API 多,新手容易一脸懵逼;
- 一旦链过长,需要注意日志和调试手段。
10.ExecutorService + Future/FutureTask
(Future/FutureTask/Callable/Runnable/线程池)
10.1 代码实现
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交多个任务
Future<String> future1 = executor.submit(() -> "结果1");
Future<String> future2 = executor.submit(() -> "结果2");
Future<String> future3 = executor.submit(() -> "结果3");
// 手动等待所有完成(繁琐易错)
List<Future<String>> futures = Arrays.asList(future1, future2, future3);
for (Future<String> future : futures) {
try {
String result = future.get(); // 阻塞等待
System.out.println(result);
} catch (Exception e) {
// 异常处理...
}
}
executor.shutdown();
10.2 基础概念解析
Runnable:最基础的任务接口,void run(),无返回值不能抛受检异常。Callable<V>:增强版任务接口,V call() throws Exception,有返回值可抛异常。Future<V>:异步结果的接口规范,定义了get/isDone/cancel等。FutureTask<V>:- 既是
Runnable又是Future(实现了RunnableFuture); - 内部持有
Callable、状态、outcome结果字段; - 是“任务包装器 + 结果存储器 + 状态机”。
- 既是
ExecutorService:线程池,负责调度执行任务。
10.3 线程池 submit(Callable) 背后发生了什么?
public class ThreadPoolExecutor {
public <T> Future<T> submit(Callable<T> task) {
// 1. 包装:Callable -> FutureTask
FutureTask<T> futureTask = new FutureTask<>(task);
// 2. 执行:FutureTask 被当成 Runnable 扔进线程池
execute(futureTask);
// 3. 返回:FutureTask 作为 Future 返回给调用者
return futureTask;
}
}
再看 FutureTask 的核心字段(简化后):
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state; // 状态机
private Callable<V> callable; // 要执行的任务
private Object outcome; // 🔥 存储结果
private volatile Thread runner; // 执行任务的线程
public void run() {
try {
V result = callable.call(); // 真正执行逻辑
set(result); // outcome = result; state = NORMAL
} catch (Throwable ex) {
setException(ex);
}
}
public V get() {
// 等到 state 变成完成态
awaitDone(...);
return report(state); // 读 outcome
}
}
时序总结:
- 你调用
executor.submit(callable); - 线程池内部用
FutureTask把Callable包装起来; - 工作线程调用
futureTask.run(),里面执行callable.call(); - 执行结果写到
outcome,状态切到NORMAL; - 主线程在
future.get()处阻塞等待,状态完成后读取结果。
一句话记忆:
Callable是“我要执行的业务逻辑”,
FutureTask是“包装业务 + 管状态 + 存结果”的小容器,
线程池执行的是FutureTask.run(),你拿到的是FutureTask这个Future。
三、总结
-
基础顺序控制(顺序最简单、上手最快)
Thread.join()- 单线程线程池
-
同步点/阶段协作(等一批人都到了再走)
CountDownLatchCyclicBarrierPhaser
-
信号/条件控制(精确谁先谁后)
SemaphoreReentrantLock + Condition
-
任务/结果编排 & 消息传递(现代项目真正在用的)
BlockingQueueExecutorService + Future/FutureTaskCompletableFuture(allOf + thenXxx)
四、面试输出的“最终版话术”结构
你可以按这个模板说(直接代入你博客里的内容):
-
先给分类大图
- “控制线程执行顺序,我习惯从 4 个维度选工具:基础顺序控制、同步点协作、信号/条件控制、任务级编排。”
-
再按顺序挑重点展开
- 基础:
join+ 单线程池,举一个最简单例子。 - 协作:重点讲
CountDownLatch,顺带一笔带过CyclicBarrier/Phaser。 - 信号:举
Semaphore的 T1→T2→T3 示例。 - 编排:重点讲
CompletableFuture.allOf+ 那个首页示例,再顺带说ExecutorService + Future/FutureTask和线程池的关系。
- 基础:
-
最后用一句“本质总结”收尾
- 本质就四个字:等、唤、排、编。
- 等 = join/latch/barrier/phaser;
- 唤 = Condition/Semaphore;
- 排 = 单线程池 / BlockingQueue;
- 编 = CompletableFuture + 线程池 + FutureTask。
994

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



