并发工具:Phaser工具

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去实现多线程任务的同步管理,因为这样可能会导致多线程的控制复杂化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值