文章目录
1 Phaser工具介绍
CountDownLatch、CyclicBarrier、Exchanger、Semaphore这几个同步工具都是JDK在1.5版本中引入的,Phaser是在JDK 1.7版本中才加入的;
Phaser同样也是一个多线程的同步助手工具,它是一个可被重复使用的同步屏障,它的功能非常类似于本章已经学习过的CyclicBarrier和CountDownLatch的合集,但是它提供了更加灵活丰富的用法和方法
CountDownLatch可以很好地控制等待多个线程执行完子任务,但是它有一个缺点,那就是内部的计数器无法重置,也就是说CountDownLatch属于一次性的,使用结束后就不能再次使用。
CyclicBarrier倒是可以重复使用,但是一旦parties在创建的时候被指定,就无法再改变。
Phaser则取两家之所长于一身引入了两者的特性。
关于CountDownLatch和和CyclicBarrier可以参考:
并发工具:CountDownLatch
并发工具:CyclicBarrier(循环屏障)
2 Phaser的基本用法(入门)
2.1 将Phaser当作CountDownLatch来使用
public static void main(String[] args) throws InterruptedException {
// 定义一个Phaser,并未指定分片数量parties,
// 此时在Phaser内部分片的数量parties默认为0,
// 后面可以通过register方法动态增加
Phaser phaser = new Phaser();
// 定义10个线程
for (int i = 1; i <= 10; i++) {
new Thread(()->{
// 首先调用phaser的register方法使得phaser内部的parties加一
phaser.register();
// 采取随机休眠的方式模拟线程的运行时间开销
try {
TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(20));
// 线程任务结束,执行arrive方法
phaser.arrive();
System.out.println(Thread.currentThread().getName() + " 执行结束.");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T-" + i ).start();
}
TimeUnit.SECONDS.sleep(10);
// 主线程也调用注册方法,此时parties的数量为11=10+1
phaser.register();
// 主线程也arrive,但是它要等待下一个阶段,等
// 待下一个阶段的前提是所有的线程都arrive,也就是phaser内部当前phase的unarrived数量为0
// 主线程已经等到前面10个线程干完活了,可以继续执行了
phaser.arriveAndAwaitAdvance();
System.out.println("主线程已经等到前面10个线程干完活了,可以继续走了");
}
输出:
T-9 执行结束.
T-4 执行结束.
T-6 执行结束.
T-1 执行结束.
T-7 执行结束.
T-8 执行结束.
T-10 执行结束.
T-2 执行结束.
T-5 执行结束.
T-3 执行结束.
主线程已经等到前面10个线程干完活了,可以继续走了
主线程等待所有的子线程运行结束之后,才会接着执行下一步的任务,这看起来是不是非常类似于CountDownLatch呢?很显然是的,就目前这样的情况来看,使用Phaser可以完全替代CountDownLatch了
- 定义了一个Phaser,该Phaser内部也维护了一个类似于CyclicBarrier的parties,但是我们在定义的时候并未指定分片parties,因此默认情况下就是0,但是这个值是可以在随后的使用过程中更改的,这就是Phaser的灵活之处了。
- 紧接着创建了10个线程,并且在线程的执行单元中第一行代码,就调用了Phaser的register方法,该方法的作用其实是让Phaser内部的分片parties加一,也就是说待10个线程分别执行了register方法之后,此时的分片parties就成了10。
- 待每一个线程执行完相应的业务逻辑之后(在我们的代码中是休眠)会调用phaser的arrive()方法,该方法的作用与CountDownLatch的countdown()方法的语义一样,代表着当前线程已经到达了这个屏障,但是它不需要等待其他线程也到达该屏障。因此该方法不是阻塞方法,执行之后会立即返回,同时该方法会返回一个整数类型的数字,代表当前已经到达的Phase(阶段)编号,这个数字默认是从0开始的
- 主线程也执行了register方法,此刻Phaser的parties就为11了,紧接着主线程执行了phaser的arriveAndAwaitAdvance方法,该方法的作用除了表示当前线程已经到达了这个屏障之外,它还会等待其他线程也到达这个屏障,然后继续前行。因此该方法是一个阻塞方法,这就非常类似于CountDownLatch的await方法了,即等待所有子线程完成任务。
注意:
在主线程进行register操作之前,请务必保证所有的子线程都能够顺利register,否则就会出现phaser只注册了一个parties,并且很快arrive的情况,因此我们在主线程进行register操作之前,需要通过休眠的方式确保所有的子线程顺利register(当然这并不是一种非常严谨的方式,更加合理的方式是在定义Phaser的时候指定parties的值)。
2.2 将Phaser当作CyclicBarrier来使用
我们也可以借助于Phaser来完成CyclicBarrier的主要功能,即所有的子线程共同到达一个barrier point
public class PhaserExample2 {
public static void main(String[] args) throws InterruptedException {
// 定义一个分片parties为0的Phaser
final Phaser phaser = new Phaser();
// 定义10个线程
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
// 子线程调用注册方法,当10个子线程都执行了register,parties将为10
phaser.register();
try {
TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(20));
System.out.println(Thread.currentThread().getName() + " 业务完成");
// 调用arriveAndAwaitAdvance方法等待所有线程arrive,然后继续前行
phaser.arriveAndAwaitAdvance();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "T-" + i).start();
}
// 休眠以确保其他子线程顺利调用register方法
TimeUnit.SECONDS.sleep(10);
// 主线程调用register方法,此时phaser内部的parties为11
phaser.register();
phaser.arriveAndAwaitAdvance();
System.out.println("主线程已经等到前面10个线程干完活了,可以继续走了");
}
}
在子线程中,我们将不再使用arrive方法表示当前线程已经完成任务,取而代之的是arriveAndAwaitAdvance方法,该方法会等待在当前Phaser中所有的part(子线程)都完成了任务才能使线程退出阻塞,当然也包括主线程自身,因为主线程也进行了register操作。
3 Phaser中的Phase(阶段)
在Phaser中可以有多个Phase(阶段),为了更好地对每一个Phase进行管理和监控,Phaser为每一个Phase都提供了对应的编号,这一点与CyclicBarrier是不一样的,后者更加注重的是循环。CyclicBarrier在所有的线程都到达barrier point之后,它才会重新开始,而Phaser则不然,只要某一个Phase的所有关联parties都arrive(到达)了,它就会从下一个Phase继续开始,除非Phaser本身已经被终止或者销毁
随着Phaser的创建,每一个Phase(阶段)中所有关联的parties个任务到达之后,Phase编号的变化。
public static void main(String[] args) {
// 定义Phaser指定初始parties为3
final Phaser phaser = new Phaser(3);
// 新定义的Phaser,Phase(阶段)编号为0
System.out.println(phaser.getPhase());
// 调用三次arrive方法,使得所有任务都arrive
phaser.arrive();
phaser.arrive();
phaser.arrive();
// 当parties个任务arrive之后,Phase(阶段)的编号就变为1
System.out.println(phaser.getPhase());
// 新增一个parties,bulkRegister(1)的方法等价于register()方法
phaser.bulkRegister(1);
// 调用四次arrive方法,使得所有任务都arrive
phaser.arrive();
phaser.arrive();
phaser.arrive();
phaser.arrive();
// 当parties个任务arrive之后,Phase(阶段)编号就变为2
System.out.println(phaser.getPhase());
}
getPhase方法获取Phaser当前的Phase(阶段)编号。根据官方文档对该方法的描述:“getPhase()方法获取当前Phaser的Phase(阶段)编号,最大的Phase(阶段)编号为Integer.MAX_VALUE,如果到达Integer.MAX_VALUE这个值,那么Phase编号将会又从0开始;当Phaser被终止的时候,调用getPhase()将返回负数,如果我们想要获得Phaser终止前的前一个Phase(阶段)编号,则可以通过getPhase()+Integer.MAX_VALUE进行计算和获取”。Phase编号在Phaser中比较重要,正因为如此,除了getPhase()方法会返回Phase(阶段)编号之外,在Phaser中,几乎所有方法的返回值都是Phase(阶段)编号
4 Phaser中的方法
4.1 register方法
register方法的主要作用是为Phaser新增一个未到达的分片,并且返回Phase(阶段)的编号,该编号与Phaser当前的Phase(阶段)编号数字是一样的,但是调用该方法时,有些时候会陷入阻塞之中,比如前一个Phase(阶段)在执行onAdvance方法时耗时较长,那么此时若有一个新的分片想要通过register方法加入到Phaser中就会陷入阻塞
public static void main(String[] args) {
Phaser phaser = new Phaser();
// 当前线程调用注册方法,返回当前Phaser的Phase(阶段)编号 ,此时为0
int phase = phaser.register();
System.out.println("phaser.getPhase():" + phaser.getPhase() + "; phase : " + phase);
// 调用arrive方法到达下一个Phase(阶段),但是arrive方法会返回当前的Phase编号还是0
phase = phaser.arrive();
// 但是getPhase方法此时返回的就是1了,下一个阶段
System.out.println("phaser.getPhase():" + phaser.getPhase() + "; phase : " + phase);
// 再次调用注册方法,当前的parties(分片)数量为2,且处于新的Phase(阶段)编号为1
phase = phaser.register();
System.out.println("phaser.getPhase():" + phaser.getPhase() + "; phase : " + phase);
}
phaser.getPhase():0; phase : 0
phaser.getPhase():1; phase : 0
phaser.getPhase():1; phase : 1
有些时候在调用register方法时会进入阻塞等待状态,原因是Phaser的onAdvance方法恰好被调用且耗时较长,那么register方法就只有等待onAdvance方法完全结束后才能执行,下面通过一个代码片段验证一下:
public static void main(String[] args) throws InterruptedException {
// 定义只有一个parties(分片)的Phaser,并且重写onAdvance方法
final Phaser phaser = new Phaser(1) {
@Override
protected boolean onAdvance(int phase, int registeredParties) {
try {
// 休眠1分钟的时间
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return super.onAdvance(phase, registeredParties);
}
};
// 启动一个新的线程,该线程的逻辑非常简单,就是调用一下arrive方法使得onAdvance方法能够执行,因为当前Phase(阶段)的所有分片任务均已到达
new Thread(phaser::arrive).start();
// 休眠,确保线程先启动
TimeUnit.SECONDS.sleep(2);
// 再次调用register方法,该方法将会陷入等待
long startTimestamp = System.currentTimeMillis();
int phaseNumber = phaser.register();
System.out.println("register ELT: " + (System.currentTimeMillis() - startTimestamp));
}
第二次的register调用会进入等待阻塞,其中耗时大概在58秒左右,加上前面休眠的2秒刚好是1分钟左右:
register ELT: 57999
原因:当Phaser的某个Phase(阶段)的所有分片任务全都抵达时,会触发onAdvance方法的调用。如果在onAdvance方法执行的过程中有新的线程要求加入Phaser,比较合理的做法就是Phaser做好收尾工作之后再接纳新的分片任务进来,否则就会出现矛盾。比如,新的分区进来返回了当前的Phase(阶段)编号,但是当前阶段在进行结束收尾操作时却没有新的分区任务什么事,所以等待是一个比较合理的设计,但是有一点需要注意的是:如果有一个线程因为执行了Phaser的register方法而进入阻塞等待状态,尤其是该线程还无法被其他线程执行中断操作,那么尽可能不要在onAdvance方法中写入过多复杂且耗时的逻辑。
4.2 bulkRegister方法
/**
该方法返回的Phase(阶段)编号同register方法,但是该方法允许注册零个或者一个以上的分片(Parties)到Phaser,
其实无论是register方法还是bulkRegister方法,背后调用的都是doRegister方法,
因此register方法的特点bulkRegister方法都会具备。
返回值
**/
public int bulkRegister(int parties)
4.3 arrive和arriveAndAwaitAdvance方法
public int arrive();
public int arriveAndAwaitAdvance()
arrive和arriveAndAwaitAdvance方法都是到达Phaser的下一个Phase(阶段):
- arrive不会等待其他分片(part)
- arriveAndAwaitAdvance则会等待所有未到达的分片(part)到达
- arrive方法返回的Phase(阶段)编号为当前的Phase(阶段)编号,原理很好理解,因为它自身不清楚其他分片(part)是否到达也无须等待其他分片(part)到达下一个Phase(阶段),因此返回Phaser当前的Phase(阶段)编号即可
- 调用arriveAndAwaitAdvance方法都会返回下一个Phase(阶段)的编号,这一点很好理解,不管怎样,当前任务分片到达的肯定是下一个Phase(阶段)。
public static void main(String[] args) {
// 定义只有两个分片(parties)的Phaser
final Phaser phaser = new Phaser(2);
// 毫无疑问当前的Phase(阶段)编号为0
assert phaser.getPhase() == 0 : "phaser current phase number is 0";
// ① 第一次调用arrive方法返回当前Phaser的Phase(阶段)编号
assert phaser.arrive() == 0 : "arrived phase number is 0";
// ② 第二次调用arrive方法返回当前Phaser的Phase(阶段)编号还是0
assert phaser.arrive() == 0 : "arrived phase number is 0";
// ③ 当前的Phaser已经处于另外一个Phase(阶段)了(当前编号已经是1了),
// 但是第二次调用的时候返回的编号为啥是1,不应该返回当前编号吗
assert phaser.getPhase() == 1 : "phaser current phase number is 1";
}
上面的代码就存在这么一个疑问:注释①处调用了arrive方法返回当前的Phase(阶段)编号,这比较符合arrive方法的语义,毕竟当前的Phaser还处在Phase(阶段)0,因为还有其他的分片未到达。当程序运行到注释②处时,所有的分片(parties)均已到达,此时Phaser的Phase(阶段)应该为1,但是第二次调用arrive方法的的时候,返回的编号却是0,这个地方需要注意一下。
4.4 arriveAndDeregister方法
/**
除了到达下一个Phase(阶段)之外,它还会将当前Phaser的分区(parties)数量减少一个。
该方法也是Phaser灵活性的一个体现,即动态减少分区(parties)数量,
同时该方法的返回值也是整数类型的数字,代表着当前Phase(阶段)的编号,
如果Phase(阶段)的编号数字为负数,则表明当前的Phaser已经被销毁。
返回值也是整数类型的数字,代表着当前Phase(阶段)的编号,
**/
public int arriveAndDeregister()
public static void main(String[] args) {
// 定义只有两个分片的Phaser
final Phaser phaser = new Phaser(2);
// 其中一个分区(part)到达,并且是Phaser注册的Parties数量减1
int i = phaser.arriveAndDeregister(); // 0
System.out.println("arrived phase number is " + i);
// 当前注册的分区(part)数量为1
System.out.println("当前注册的分区(part)数量为 " + phaser.getRegisteredParties());
// 当前的Phaser Phase(阶段)编号为0
System.out.println("当前的Phaser Phase(阶段)编号为 " + phaser.getPhase());
// 调用arriveAndAwaitAdvance方法,该方法始终会返回下一个Phase(阶段)编号
System.out.println("调用arriveAndAwaitAdvance方法,该方法始终会返回下一个Phase(阶段)编号 " + phaser.arriveAndAwaitAdvance());
// 当前的Phaser Phase(阶段)编号为1
System.out.println("当前注册的分区(part)数量为 " + phaser.getRegisteredParties());
}
arrived phase number is 0
当前注册的分区(part)数量为 1
当前的Phaser Phase(阶段)编号为 0
调用arriveAndAwaitAdvance方法,该方法始终会返回下一个Phase(阶段)编号 1
当前注册的分区(part)数量为 1
4.5 awaitAdvance与awaitAdvanceInterruptibly方法
awaitAdvance(int phase):
awaitAdvanceInterruptibly(int phase)
awaitAdvanceInterruptibly(int phase, long timeout, TimeUnit unit): 该方法同上,除了增加了可被中断的功能之外,还具备超时的功能,这就需要我们在调用的时候对超时时间进行设置了。
awaitAdvance方法的主要作用是等待与Phaser关联的分片(part)都到达某个指定的Phase(阶段)编号,如果有某个分片任务未到达,那么该方法会进入阻塞状态,这有点类似于CountDownLatch的await方法,虽然该方法是Phaser提供的方法,但是它并不会参与对arrive与unarrive分片(part)的运算和维护,如果入参phase与当前Phaser的phase(阶段)编号不一致,则会立即返回,如果当前的Phaser已经被销毁,那么它同样不会工作,并且调用该方法的返回值为负数
awaitAdvanceInterruptibly 增加了可中断功能
final Phaser phaser = new Phaser(1);
Thread thread = new Thread(() ->
{
// 断言当前的phase(阶段)编号为0
assert phaser.getPhase() == 0;
// 调用awaitAdvance方法,顺便将Phaser当前的phase编号传递进去
int phaseNumber = phaser.awaitAdvance(phaser.getPhase());
// 只有当Phaser所关联的所有分片任务都arrive了,awaitAdvance方法才会退出阻塞,并 且返回下一个phase(阶段)编号
assert phaseNumber == 1;
});
thread.start();
TimeUnit.MINUTES.sleep(1);
// 1分钟后仅有的一个分片任务arrive
assert phaser.arriveAndAwaitAdvance() == 1;
assert phaser.getPhase() == 1;
传递了错误的phase编号awaitAdvance方法并不会抛出错误,因此在使用的时候一定要注意,如果某个phase(阶段)所有的关联分片任务都没有到达,那么此刻调用awaitAdvance方法的线程将会陷入阻塞状态,并且还会无法对其执行中断操作。
5 Phaser层级关系
定义Phaser的时候也可以为其指定父Phaser,当我们在创建某个Phaser的时候若指定了父Phaser,那么它将具有如下这些特性:
- 子Phaser当前的Phase(阶段)编号会以父Phaser的编号为准。
- 父Phaser的所有分片数量=父Phaser分片数量的自身注册数量+所有子Phaser的分片注册数量之和。
- 调用当前Phaser的arriveAndAwaitAdvance方法时,首先会调用父Phaser的对应方法。
- 直接调用子Phaser的arrive方法时,在某些情况下会出现bad arrive的错误。
通常情况下,我们不会借助有层级关系的Phaser去实现多线程任务的同步管理,因为这样可能会导致多线程的控制复杂化