请谈谈在 Java 并发编程中,有哪些手段可以保证多个线程按照预期的先后顺序执行?请分别说明其适用场景与局限性。
考察说明
考查对 Java 并发中线程顺序控制手段的掌握,包括 join、CountDownLatch、Semaphore、Future、CompletableFuture 等,以及其机制和适用场景。
回答思路
- 【回答框架 1】线程顺序控制的核心问题是协调多个线程的执行时机。Java 中最简单的方式是使用 Thread 类的 join() 方法,它让当前线程等待被 join 的线程终止后再继续执行。例如,线程 A 中调用 threadB.join() 会使 A 等待 B 执行完毕再继续,从而实现串行化。适用场景是简单的先后依赖,但若线程数量多或关系复杂,代码会变得冗长且耦合度高。
- 【回答框架 2】借助 JUC 工具类可以更灵活地控制。CountDownLatch 允许一个或多个线程等待一组事件完成,初始化时设置计数器,每个线程完成后调用 countDown() 减一,等待线程通过 await() 阻塞直到计数归零。适用于等待 N 个线程都完成后再执行下一步,如主线程等待多个子线程初始化完成。
- 【回答框架 3】Semaphore 通过信号量控制同时访问的线程数,也可以用于顺序控制。它通过 acquire() 和 release() 协调,但通常用于限流或互斥,直接用于顺序控制时设计较绕,不如 CountDownLatch 直观。适用场景是控制并发度,而非严格的先后顺序。
- 【回答框架 4】更现代的方式是使用 Future 和 CompletableFuture。实现 Runnable 或 Callable 提交到线程池后,通过 future.get() 阻塞获取结果,从而完成顺序依赖。CompletableFuture 提供了 thenApply、thenCompose 等链式方法,可以声明式地编排异步任务的先后顺序。例如,thenRun 在前一个任务完成后执行后续动作,支持组合多个任务。
- 【回答框架 5】实际项目中通常优先使用 CompletableFuture 进行流程编排,它可读性和组合能力强。其他方式如线程池配合 BlockingQueue 也能实现生产者消费者之间的顺序,但不适合任务间复杂依赖。选型时要考虑线程数量、异常处理和资源的确定性,避免直接使用 join 造成主线程长时间阻塞。
- 【关键点 1】Thread.join() 实现简单的先后等待,但不能灵活组合。
- 【关键点 2】CountDownLatch 适用于等待 N 个线程完成,计数一次即失效。
- 【关键点 3】Semaphore 主要用于控制并发数,顺序控制非其主要用途。
- 【关键点 4】CompletableFuture 可声明式编排异步任务链,支持异常处理。
- 【关键点 5】线程池提交任务时,无法保证执行顺序,需依赖 Future 或 CompletableFuture 等机制。
- 【易错点 1】不要用 Thread.sleep() 猜测顺序,定时不可控且浪费资源。
- 【易错点 2】CountDownLatch 为一次性的,若要重复使用需用 CyclicBarrier。
- 【易错点 3】CompletableFuture 默认使用 ForkJoinPool.commonPool,可能影响其他任务,可指定自定义线程池。