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控制多线程顺序执行的资料请关注脚本之家其它相关文章!
