29.线程协同:T3 如何优雅地等到 T1/T2 执行完?

线程协同:T3 如何优雅地等到 T1/T2 执行完?

面试高频问题:
T1、T2 并行执行,要求 T3 必须在 T1、T2 都完成后再执行。
本质是考你:多线程协同机制 + JUC 工具类 + Future/FutureTask/线程池的理解


先场景抽象理解,再自己具象出场景答题(做到场景可控)

先把场景抽象出来:

“等三个都跑完再继续”的核心就是:
把每个子任务包装成一个可等待的句柄(handle),然后在主线程上统一等待这些句柄完成。

对应不同的方法,再用一个简单的,可变化的通用 实际项目场景 具象化串起来。
比如:

项目实例:异步首页查询对应模块任务卡片数量

  • 模块一:查询“待办数”
  • 模块二:查询“入库异常数”
  • 模块三:查询“出库异常数”

要求:三个查询并行,都查完后再组装首页数据返回。


一、理解线程协同的不同需求,做到心中有层级

  1. 先区分“线程顺序”还是“任务顺序”

    • 有的场景关注的是:T1 → T2 → T3 这个线程先后,比如多线程交替打印 ABC;
    • 更多真实业务,其实只关心“任务 A、任务 B、任务 C 的执行顺序”,并不在意具体是哪个线程跑的。
    • 这会直接影响你是用 join / Condition 这类“线程视角”的工具,还是用单线程池 / CompletableFuture 这种“任务视角”的工具。
  2. 再看要不要结果、要不要组合结果

    • 如果只是“按顺序做几件事”,不关心返回值(例如按顺序打日志、发通知),可以用最简单的协作手段;
    • 如果要并发执行 + 汇总多个返回结果(例如首页并发查多个模块卡片数量),就需要 ExecutorService + FutureCompletableFuture.allOf 这种“带结果的协作方案”。
  3. 判断是“一次性顺序”还是“多阶段协同”

    • 有的场景只需要:一批任务都执行完,再继续下一步,典型代表是:joinCountDownLatchallOf
    • 有的场景是:要分“阶段一 / 阶段二 / 阶段三”,每一阶段内部并行,阶段之间要同步,这时候就要考虑 CyclicBarrierPhaser 一类的“分阶段协同工具”。
  4. 最后考虑复杂度:简单业务用简单工具,复杂协作再上锁/条件队列

    • 如果只是简单顺序控制,join、单线程池、CountDownLatchCompletableFuture 就足够了;
    • 只有在“需要精确点名唤醒谁”“多线程交替打印”“复杂流水线”的少数场景下,才有必要上 SemaphoreReentrantLock+Condition 这种更底层的控制手段。

把这四个维度讲完后,再顺着说:

  • 基础顺序控制:join、单线程池;
  • 一次性等待一批任务:CountDownLatchCompletableFuture.allOfExecutorService + Future/FutureTask
  • 多阶段协同:CyclicBarrierPhaser
  • 精细控制先后:SemaphoreReentrantLock+ConditionBlockingQueue

二、 三类-- 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() 空时阻塞;
  • 常见类型:ArrayBlockingQueueLinkedBlockingQueue

用两个队列实现 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 的对比:

能力FutureCompletableFuture
有无返回值
获取结果方式get() 阻塞get()/join() + 回调 + 链式处理
多任务组合手动管理列表allOf/anyOf/thenCombine/thenCompose
异常处理ExecutionExceptionexceptionally/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
    }
}

时序总结:

  1. 你调用 executor.submit(callable)
  2. 线程池内部用 FutureTaskCallable 包装起来;
  3. 工作线程调用 futureTask.run(),里面执行 callable.call()
  4. 执行结果写到 outcome,状态切到 NORMAL
  5. 主线程在 future.get() 处阻塞等待,状态完成后读取结果。

一句话记忆:

Callable 是“我要执行的业务逻辑”,
FutureTask 是“包装业务 + 管状态 + 存结果”的小容器,
线程池执行的是 FutureTask.run(),你拿到的是 FutureTask 这个 Future


三、总结

  1. 基础顺序控制(顺序最简单、上手最快)

    • Thread.join()
    • 单线程线程池
  2. 同步点/阶段协作(等一批人都到了再走)

    • CountDownLatch
    • CyclicBarrier
    • Phaser
  3. 信号/条件控制(精确谁先谁后)

    • Semaphore
    • ReentrantLock + Condition
  4. 任务/结果编排 & 消息传递(现代项目真正在用的)

    • BlockingQueue
    • ExecutorService + Future/FutureTask
    • CompletableFuture(allOf + thenXxx)

四、面试输出的“最终版话术”结构

你可以按这个模板说(直接代入你博客里的内容):

  1. 先给分类大图

    • “控制线程执行顺序,我习惯从 4 个维度选工具:基础顺序控制、同步点协作、信号/条件控制、任务级编排。”
  2. 再按顺序挑重点展开

    • 基础:join + 单线程池,举一个最简单例子。
    • 协作:重点讲 CountDownLatch,顺带一笔带过 CyclicBarrier / Phaser
    • 信号:举 Semaphore 的 T1→T2→T3 示例。
    • 编排:重点讲 CompletableFuture.allOf + 那个首页示例,再顺带说ExecutorService + Future/FutureTask和线程池的关系。
  3. 最后用一句“本质总结”收尾

    • 本质就四个字:等、唤、排、编
    • 等 = join/latch/barrier/phaser;
    • 唤 = Condition/Semaphore;
    • 排 = 单线程池 / BlockingQueue;
    • 编 = CompletableFuture + 线程池 + FutureTask。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值