【大白话说Java面试题 第119题】【并发篇】第19题:多线程如何顺序执行?

📌 异常处理Java开发基于Spring Boot的异常处理框架设计:电商系统业务异常建模与全局统一响应实现

第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 实现
    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");
        }
    }
    
    注意:纯自旋浪费 CPU,生产环境应使用 LockSupport.park/unparkCondition
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。”(没有对比和选型)
    • 高分回答

      "多线程顺序执行有六种主流方案,选型取决于顺序粒度和业务需求:

      1. join():线程级顺序,简单但线程不可复用,适合测试和脚本;
      2. 单线程线程池:任务级顺序,线程复用,适合日志写入、消息顺序消费;
      3. CountDownLatch:事件级顺序,线程可并行启动按需等待,适合流水线;
      4. CompletableFuture:函数级顺序,支持链式调用、返回值、异常处理,适合微服务编排;
      5. Semaphore:许可级顺序,控制并发数为 1,适合限流场景;
      6. CyclicBarrier:阶段级顺序,多线程分阶段协同,适合 MapReduce。

      现代 Java 项目优先推荐 CompletableFuture,它兼顾了顺序控制、异步非阻塞、异常处理和返回值。"

  • 追问 2:“join() 和 CountDownLatch 有什么区别?”

    • 高分回答

      "核心区别在于等待的对象和线程复用性

      1. 等待对象join() 等待目标线程 TERMINATED;CountDownLatch 等待计数器归零(事件信号);
      2. 线程复用join() 要求线程结束,不可复用;CountDownLatch 线程只需 countDown,可继续执行其他任务;
      3. 多对多join() 只能一对一等待;CountDownLatch 支持多线程等待同一事件(如 10 个线程等 3 个初始化完成);
      4. 返回值:两者都不支持返回值,但 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 操作,整个队列都会阻塞。此时应考虑:

      1. 将 IO 操作异步化;
      2. 使用 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 怎么实现并行执行、全部完成后汇总?”

    • 高分回答

      "使用 allOfanyOf 组合多个 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→CCountDownLatch事件驱动,线程可复用
微服务编排CompletableFuture链式调用,支持异常和返回值
多阶段并行计算CyclicBarrier阶段同步,可循环使用
限流顺序执行Semaphore(1)控制并发数为 1
复杂依赖图CompletableFuture + allOf灵活组合并行/串行

💡 面试官想要的满分总结

多线程顺序执行不是"让线程排队",而是控制执行流的依赖关系。现代 Java 开发中,CompletableFuture 是首选方案——它用函数式编程替代了传统的线程管理,支持链式顺序编排、并行组合、异常处理和返回值,完美适配微服务架构。

但经典方案仍有其价值:join() 适合简单测试;单线程线程池适合顺序任务队列;CountDownLatch 适合流水线事件驱动;CyclicBarrier 适合分阶段并行计算。选型核心:任务同质用队列,任务异质用编排,阶段协同用屏障

生产避坑:单线程池用有界队列防 OOM;CompletableFuture 自定义线程池防 ForkJoin 阻塞;CountDownLatch 一次性不可复用;join() 先全部 start 再依次 join 才能并行。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI人工智能+电脑小能手

若对您有所帮助,请点点关注哟~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值