Java中控制多线程顺序执行的六种实现方案
作者:Bug改不动了
一、线程顺序执行的核心挑战
在多线程编程中,线程的执行顺序本质上是不确定的,由操作系统调度器决定。但在某些业务场景中,我们需要确保线程按照特定顺序执行,例如:
- 分阶段任务处理(先初始化,再加载,最后执行)
- 事件的有序处理(保证事件处理的先后顺序)
- 资源的有序访问(避免竞争条件)
下面介绍的6种方法都能解决这个问题,但各有特点和适用场景。
二、6种实现方法详解
方法1:join() - 简单直接的阻塞方案
实现原理:
join()
方法会使当前线程等待目标线程执行完毕。通过在主线程中依次调用各个线程的join()
,可以实现严格的顺序执行。
public class JoinExample { public static void main(String[] args) { Thread t1 = new Thread(() -> { System.out.println("第一阶段任务执行"); // 模拟任务执行时间 try { Thread.sleep(500); } catch (InterruptedException e) {} }); Thread t2 = new Thread(() -> System.out.println("第二阶段任务执行")); Thread t3 = new Thread(() -> System.out.println("第三阶段任务执行")); try { System.out.println("启动第一阶段任务"); t1.start(); t1.join(); // 主线程在此等待t1完成 System.out.println("启动第二阶段任务"); t2.start(); t2.join(); // 等待t2完成 System.out.println("启动第三阶段任务"); t3.start(); t3.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("任务执行被中断"); } } }
优点:
- 实现简单直观
- 不需要额外同步工具
缺点:
- 会阻塞主线程
- 不够灵活,难以应对复杂场景
适用场景:简单的线性任务流,且可以接受阻塞主线程的情况。
方法2:单线程线程池 - 优雅的任务队列方案
实现原理:
通过Executors.newSingleThreadExecutor()
创建单线程的线程池,自然保证任务按提交顺序执行。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SingleThreadExecutorExample { public static void main(String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { System.out.println("准备数据"); // 模拟耗时操作 try { Thread.sleep(1000); } catch (InterruptedException e) {} }); executor.execute(() -> System.out.println("处理数据")); executor.execute(() -> System.out.println("保存结果")); executor.shutdown(); // 等待所有任务完成 while (!executor.isTerminated()) {} System.out.println("所有任务已完成"); } }
优点:
- 自动管理线程生命周期
- 支持任务队列
- 避免手动创建线程
缺点:
- 无法灵活控制中间状态
- 单线程可能成为性能瓶颈
适用场景:需要顺序执行但可能动态添加任务的场景。
方法3:CountDownLatch - 灵活的同步屏障方案
实现原理:
CountDownLatch
通过计数器实现线程等待,允许一个或多个线程等待其他线程完成操作。
import java.util.concurrent.CountDownLatch; public class CountDownLatchExample { public static void main(String[] args) { // 第一个闸门,初始为1表示t1可以直接执行 CountDownLatch latch1 = new CountDownLatch(1); // 第二个闸门,t2需要等待 CountDownLatch latch2 = new CountDownLatch(1); Thread t1 = new Thread(() -> { System.out.println("数据库连接建立"); latch1.countDown(); // t1完成后打开闸门1 }); Thread t2 = new Thread(() -> { try { latch1.await(); // 等待闸门1打开 System.out.println("查询用户数据"); latch2.countDown(); // t2完成后打开闸门2 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread t3 = new Thread(() -> { try { latch2.await(); // 等待闸门2打开 System.out.println("生成报表"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // 故意打乱启动顺序测试 t3.start(); t2.start(); t1.start(); } }
优点:
- 灵活控制多个线程的依赖关系
- 支持一对多、多对多同步
- 不阻塞主线程
缺点:
- 需要创建多个CountDownLatch对象
- 计数器不可重置
适用场景:分阶段任务,特别是多阶段有复杂依赖关系的场景。
方法4:ReentrantLock与Condition - 精准控制的等待/通知机制
实现原理:
ReentrantLock
配合Condition
提供了比synchronized
更灵活的线程通信机制。每个Condition
对象实质上是一个独立的等待队列,可以实现精确的线程唤醒控制。
import java.util.concurrent.locks.*; public class ReentrantLockConditionExample { // 可重入锁 private static Lock lock = new ReentrantLock(true); // 公平锁 // 三个条件变量 private static Condition condition1 = lock.newCondition(); private static Condition condition2 = lock.newCondition(); private static Condition condition3 = lock.newCondition(); // 状态标志 private static volatile int flag = 1; public static void main(String[] args) { Thread t1 = new Thread(() -> task(1, 2, condition1, condition2)); Thread t2 = new Thread(() -> task(2, 3, condition2, condition3)); Thread t3 = new Thread(() -> task(3, 1, condition3, condition1)); t1.start(); t2.start(); t3.start(); // 模拟运行后停止 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.exit(0); } private static void task(int currentFlag, int nextFlag, Condition waitCondition, Condition signalCondition) { while (true) { lock.lock(); try { // 检查是否轮到自己执行 while (flag != currentFlag) { waitCondition.await(); // 释放锁并等待 } System.out.println("Thread " + currentFlag + " 正在执行"); Thread.sleep(1000); // 模拟处理时间 // 更新状态并唤醒下一个线程 flag = nextFlag; signalCondition.signal(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } } }
关键点解析:
- 公平锁:构造
ReentrantLock
时传入true参数创建公平锁,减少线程饥饿 - 条件变量:每个线程有自己专属的
Condition
,避免无效唤醒 - volatile变量:确保状态标志的可见性
- await/signal:精确控制线程的等待和唤醒
优点:
- 最精确的线程控制能力
- 支持公平性配置
- 可中断的等待机制
- 支持多个等待条件
缺点:
- 实现复杂度高
- 需要手动管理锁的获取和释放
- 容易遗漏unlock导致死锁
适用场景:
- 需要精确控制线程执行顺序的复杂场景
- 多条件等待的线程协作
- 对公平性有要求的场景
方法5:Semaphore - 基于许可的同步控制
实现原理:
Semaphore
通过维护一组许可(permits)来控制线程访问。初始化时指定许可数量,线程通过acquire()
获取许可,通过release()
释放许可。
import java.util.concurrent.Semaphore; public class SemaphoreExample { // 初始化信号量:t1可以直接运行,t2和t3需要等待 private static Semaphore s1 = new Semaphore(1); private static Semaphore s2 = new Semaphore(0); private static Semaphore s3 = new Semaphore(0); public static void main(String[] args) { Thread t1 = new Thread(() -> { try { s1.acquire(); // 获取许可 System.out.println("线程1:数据加载"); Thread.sleep(1000); s2.release(); // 释放t2的许可 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread t2 = new Thread(() -> { try { s2.acquire(); System.out.println("线程2:数据处理"); Thread.sleep(1000); s3.release(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); Thread t3 = new Thread(() -> { try { s3.acquire(); System.out.println("线程3:结果保存"); Thread.sleep(1000); s1.release(); // 循环执行时可重新开始 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // 启动线程 t1.start(); t2.start(); t3.start(); // 允许程序运行一段时间后退出 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }
进阶用法:
- 公平模式:
new Semaphore(1, true)
- 批量获取:
acquire(int permits)
- 非阻塞获取:
tryAcquire()
- 超时控制:
tryAcquire(long timeout, TimeUnit unit)
优点:
- 灵活控制并发度
- 支持许可的申请和释放
- 可实现资源池等复杂模式
缺点:
- 需要仔细设计许可数量
方法6:CompletableFuture - 函数式异步编程
实现原理:
CompletableFuture
是Java 8引入的增强版Future,支持函数式编程风格的任务编排。通过thenRun
、thenApply
等方法链式组合多个异步任务。
import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class CompletableFutureExample { public static void main(String[] args) { // 创建异步任务链 CompletableFuture<Void> taskChain = CompletableFuture .runAsync(() -> { System.out.println("任务1:初始化系统"); sleep(1000); }) .thenRunAsync(() -> { System.out.println("任务2:加载配置"); sleep(1500); }) .thenRunAsync(() -> { System.out.println("任务3:启动服务"); sleep(500); }) .thenRunAsync(() -> { System.out.println("任务4:运行监控"); }); // 等待所有任务完成 try { taskChain.get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } private static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
优点:
- 声明式编程风格
- 强大的任务组合能力
- 内置异常处理机制
- 与现代Java特性完美集成
缺点:
- 学习曲线较陡峭
- 调试相对困难
- Java 8+才支持
适用场景:
- 现代Java应用开发
- 复杂的异步任务编排
- 需要组合多个异步结果的场景
- 响应式编程基础
三、常见问题与解决方案
Q1:为什么await()要在while循环中调用?
A:这是为了防止"虚假唤醒"(spurious wakeup),即线程可能在没有收到通知的情况下被唤醒。while循环会重新检查条件,确保条件真正满足。
Q2:如何避免死锁?
A:遵循以下原则:
- 按固定顺序获取多个锁
- 设置锁超时时间
- 避免在锁中调用外部方法
- 使用tryLock()替代lock()
以上就是Java中控制多线程顺序执行的六种实现方案的详细内容,更多关于Java控制多线程顺序执行的资料请关注脚本之家其它相关文章!