Java多线程开发中,线程执行顺序的不确定性常常会导致业务结果不符合预期,尤其是需要多个线程按照固定顺序交替执行任务的场景,比如两个线程分别打印奇数和偶数、生产者消费者按节奏交替工作等。Semaphore作为并发包java.util.concurrent里的同步辅助类,通过维护一组许可证来控制线程对共享资源的访问权限,借助它的特性可以很方便地实现多线程的精确交替同步。

Semaphore核心原理
Semaphore内部管理一个许可集,线程在执行前需要先获取许可,如果许可数量足够就可以继续执行,执行完成后需要释放许可;如果许可不足,线程就会被阻塞直到其他线程释放许可。我们可以通过初始化不同的许可数量,配合线程的获取和释放逻辑,来约束线程的执行顺序。
常用的核心方法有两个:
- acquire():从Semaphore中获取一个许可,如果没有可用许可则当前线程阻塞
- release():释放一个许可,将其返回给Semaphore,唤醒等待的线程
两个线程交替执行的实现示例
我们以两个线程交替打印1到10的数字为例,线程A打印奇数,线程B打印偶数,要求严格按照1、2、3、4这样的顺序输出。
实现思路是初始化两个Semaphore,分别控制两个线程的执行权限:给线程A的Semaphore初始化1个许可,给线程B的Semaphore初始化0个许可。线程A执行前获取自己的许可,执行完成后释放线程B的许可;线程B执行前获取自己的许可,执行完成后释放线程A的许可,这样就能保证两个线程交替执行。
完整代码实现
import java.util.concurrent.Semaphore;
public class SemaphoreAlternateDemo {
// 控制线程A的Semaphore,初始1个许可,先让线程A执行
private static Semaphore semaphoreA = new Semaphore(1);
// 控制线程B的Semaphore,初始0个许可,线程B初始不能执行
private static Semaphore semaphoreB = new Semaphore(0);
// 共享的计数变量
private static int count = 1;
// 最大计数到10
private static final int MAX_COUNT = 10;
public static void main(String[] args) {
// 线程A,打印奇数
Thread threadA = new Thread(() -> {
try {
while (count <= MAX_COUNT) {
// 获取线程A的许可
semaphoreA.acquire();
if (count <= MAX_COUNT) {
System.out.println("线程A打印:" + count);
count++;
}
// 释放线程B的许可,让线程B可以执行
semaphoreB.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Thread-A");
// 线程B,打印偶数
Thread threadB = new Thread(() -> {
try {
while (count <= MAX_COUNT) {
// 获取线程B的许可
semaphoreB.acquire();
if (count <= MAX_COUNT) {
System.out.println("线程B打印:" + count);
count++;
}
// 释放线程A的许可,让线程A可以执行
semaphoreA.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Thread-B");
// 启动两个线程
threadA.start();
threadB.start();
}
}
代码执行结果
运行上述代码后,输出结果如下:
线程A打印:1 线程B打印:2 线程A打印:3 线程B打印:4 线程A打印:5 线程B打印:6 线程A打印:7 线程B打印:8 线程A打印:9 线程B打印:10
多个线程交替执行的扩展
如果需要三个或更多线程交替执行,只需要对应增加Semaphore的数量,每个线程持有前一个线程的释放许可即可。比如三个线程A、B、C交替执行,初始化semaphoreA为1,semaphoreB为0,semaphoreC为0;A执行完释放B的许可,B执行完释放C的许可,C执行完释放A的许可,就能形成循环交替的逻辑。
注意事项
- Semaphore的许可数量初始化要符合交替逻辑,避免出现死锁,比如两个线程的场景不能两个都初始化为0,否则两个线程都会阻塞无法执行
- 线程执行完成后一定要释放对应的许可,否则会导致后续线程永远无法获取许可,程序卡死
- 如果共享变量需要被多个线程修改,要注意变量的可见性,上述示例中count变量可以用volatile修饰,或者使用AtomicInteger来保证原子性,避免可见性问题导致的计数错误
- acquire方法会响应中断,如果线程被中断会抛出InterruptedException,实际开发中要做好异常处理,避免异常被吞掉导致问题难以排查
适用场景总结
这种基于Semaphore的交替同步方案适合线程数量固定、执行顺序明确的场景,比如固定节奏的任务轮询、按批次交替处理数据的场景。如果线程数量不固定或者执行顺序需要动态调整,可能需要结合其他同步工具比如Condition、Lock等来实现,开发者可以根据实际场景选择合适的方案。