并发编程原理与实战(四)经典并发协同方式synchronized与wait+notify详解
并发编程原理与实战(五)经典并发协同方式伪唤醒与加锁失效原理揭秘
并发编程原理与实战(六)详解并发协同利器CountDownLatch
并发编程原理与实战(七)详解并发协同利器CyclicBarrier
并发编程原理与实战(十一)并发协同利器Phaser之方法详解
上一篇讲解了Phaser的几类核心方法,本文来讲解下怎么用这些方法,分析如何将这些方法运用到具体的实际场景中。
多阶段多线程调用接口问题梳理
Phaser的主要使用场景是多个线程多个阶段并发协同运行,并且可以调整每个阶段的线程数量,如果把Phaser用在“不同阶段(时段)多线程调用接口“这个场景中,我们列一下可能会产生哪些问题:
1、不同阶段的屏障怎么表示?
2、不同阶段的阶段号怎么表示?
3、如何指定不同阶段的线程数量?如何在某个阶段动态增加线程数量?
4、每个阶段多个线程同时调用接口怎么实现?
5、当前阶段调用完成后如何进入到下一个阶段?
6、如何在每个阶段完成后,自定义一个动作实现指定业务逻辑?如打印当前的阶段号,输出当前阶段的参与线程数量等。
7、在某个阶段增加参与线程后,如何让这些增加的线程不参与后面阶段的线程的调用?
多阶段多线程调用接口问题分析
1、通过前面两篇文章我们已经知道,Phaser是一个可重复使用的同步屏障,功能类似于CyclicBarrier,创建一个Phaser对象就是创建一个屏障,可以指定参与线程的数量,既然可以重复使用,那么所有阶段都是使用同一个屏障,并不需要每个阶段设置一个屏障。所以不同阶段的屏障就用一个Phaser对象来表示。
2、创建Phaser屏障时,默认的阶段号从0开始,最大值是Integer.MAX_VALUE,所以不同阶段的阶段号是Phaser内部自动维护的,并不需要我们手动设置变量来表示。初始阶段的参与线程都完成了当前阶段的任务后阶段号自动增加。
3、创建Phaser屏障时,初始阶段可以指定参与线程数量,后面的阶段通过注册( register()方法或bulkRegister(int parties)方法)与注销类方法(arriveAndDeregister()方法)来灵活调整参与线程的数量,无需每个阶段用一个变量来指定参与线程数量。可以通过调用isTerminated()方法判断屏障的运行状态和获取当前的阶段号,在指定的阶段增加参与者。
4、调用arriveAndAwaitAdvance()方法表明线程已经达到屏障点完成了当前阶段的任务,并等待其他线程到达,类似CyclicBarrier的await()方法,当全部参与线程到达后自动触发“同时调用”。
5、当前阶段的全部参与线程调用了arriveAndAwaitAdvance()方法后,自动进入下一个阶段。
6、通过前文我们已经知道,重写onAdvance(int phase, int registeredParties)方法可以控制阶段转换时的行为逻辑,所以可以通过该方法实现在阶段完成时的业务逻辑。
7、注销参与线程的方法只有arriveAndDeregister()一个,如果想让当前线程不参与后面阶段的运行,可以在任务完后调用该方法。
多阶段多线程调用接口编码实现
分析完成上述问题以及解决办法后,下面我们用代码实现。
public class PhaseSyncDemo {
// 总阶段数
private static final int PHASE_COUNT = 3;
// 初始参与线程数
private static final int THREAD_COUNT = 4;
// 动态增加的参与线程数
private static final int ADD_THREAD_COUNT = 3;
public static void main(String[] args) {
//创建多阶段屏障
Phaser phaser = new Phaser(THREAD_COUNT) {
//重写onAdvance方法,定义阶段完成时的动作
@Override
protected boolean onAdvance(int phase, int registeredParties) {
System.out.println("\n====== 当前阶段" + phase + "," + registeredParties + "个参与者,在屏障点集合完成,准备发起调用 ======");
//所有阶段完成或者参与者数量为0,屏障终止
return phase >= PHASE_COUNT - 1 || registeredParties == 0;
}
};
//创建3个阶段,每个阶段初始化3个参与线程
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int phase = 0; phase < PHASE_COUNT; phase++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程到达阶段屏障点并等待
phaser.arriveAndAwaitAdvance();
//多个线程同时到达屏障点后同步调用接口
callApi();
}
}, "Thread-" + i).start();
}
//判断相位器是否已经终止
while (!phaser.isTerminated()) {
//判断当前阶段号是否为1,为1则动态增加3个参与线程
if (phaser.getPhase() == 1) {
phaser.bulkRegister(ADD_THREAD_COUNT);
for(int i=0;i<ADD_THREAD_COUNT;i++) {
new Thread(() -> {
//动态增加的线程到达屏障点并等待
phaser.arriveAndAwaitAdvance();
callApi();
//动态增加的线程完成任务后注销离开,不参与下一个阶段
phaser.arriveAndDeregister();
}, "Add-Thread-" + i).start();
}
break;
}
}
}
private static void callApi() {
System.out.println("时间戳" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + "调用接口");
}
}
运行结果:
====== 当前阶段0,4个参与者,在屏障点集合完成,准备发起调用 ======
时间戳1748494161912,Thread-1调用接口
时间戳1748494161912,Thread-0调用接口
时间戳1748494161912,Thread-2调用接口
时间戳1748494161912,Thread-3调用接口
====== 当前阶段1,7个参与者,在屏障点集合完成,准备发起调用 ======
时间戳1748494162918,Thread-2调用接口
时间戳1748494162918,Add-Thread-0调用接口
时间戳1748494162918,Thread-0调用接口
时间戳1748494162918,Add-Thread-1调用接口
时间戳1748494162918,Thread-3调用接口
时间戳1748494162918,Add-Thread-2调用接口
时间戳1748494162918,Thread-1调用接口
====== 当前阶段2,4个参与者,在屏障点集合完成,准备发起调用 ======
时间戳1748494163933,Thread-2调用接口
时间戳1748494163933,Thread-3调用接口
时间戳1748494163933,Thread-1调用接口
时间戳1748494163933,Thread-0调用接口
从运行结果可以看出,阶段0只有初始化的4个线程参与,阶段1的后增加了3个参与线程,这三个线程只参与阶段1的调用,阶段2又恢复回初始化的4个线程参与。
从代码中可以看出,当创建Phaser屏障对象时指定了参与线程的数量后,直接创建线程并在线程的任务代码中通过调用屏障的arriveAndAwaitAdvance()方法表明线程参与进来了,并不需要显示的调用register()方法或者bulkRegister(int parties)方法注册线程;而在阶段运行的过程中动态添加的线程,则需要先调用注册方法进行注册。
总结
到目前为止,我们已经学习了CountDownLatch、CyclicBarrier、Semaphore、Phaser这四个多线程并发协同工具类,下面总结下这四个并发协同工具类的使用场景和特性。
工具类 | 使用场景 | 特性 |
---|---|---|
CountDownLatch | 主线程等待一组子线程执行完成后继续执行、一组线程需要等待统一的指令后同时执行 | 倒计时锁存器,单向计数器递减,不可重复使用,无动态线程调整参与线程数量的能力 |
CyclicBarrier | 一组线程在屏障点互相等待,全部到达后同时释放线程继续执行;需要多次执行“等待-释放”动作的场景 | 固定的参与线程数,屏障支持重置和重复使用 |
Semaphore | 通过信号量控制并发访问共享资源的线程数量 | 无分阶段运行能力,仅仅控制并发的线程数量,支持公平和非公平模式排队访问共享资源 |
Phaser | 一组线程在屏障点互相等待,全部到达后同时释放线程继续下一个阶段的执行,需要运行中动态注册或注销参与线程 | 具有和CyclicBarrier一样的特性,支持无限阶段推进、分层结构和运行时动态调整参与者,灵活性最高 |