Phaser 有人退出后为什么仍未推进:数清当前轮的参与资格
两个工作者都已经报告“这一轮结束”,协调线程却仍然卡在阶段零。先数注册参与者:如果协调线程自己也是一个 party,它没有到达,阶段本来就不该推进。Phaser 记的是注册与到达计数,不是“线程池里有几个线程”。
离开也不是把线程对象从某个集合里删除。arriveAndDeregister 同时表达当前阶段已经到达,并减少后续阶段的注册数量。留下来的参与者仍然必须完成自己的到达,剩余人数不会因为有人退出就自动归零。
下面让一名工作者只参加第一轮,另一名继续到第二轮。门闩固定观察时点,避免打印计数时线程已经进入下一阶段。Java 17 保存为 DynamicParties.java 后执行 java DynamicParties.java:
import java.util.concurrent.*;
public class DynamicParties {
static void await(CountDownLatch latch) throws InterruptedException {
if (!latch.await(3, TimeUnit.SECONDS)) throw new IllegalStateException("coordination timeout");
}
public static void main(String[] args) throws Exception {
Phaser phaser = new Phaser(1); // Coordinator is one registered party.
phaser.bulkRegister(2);
ExecutorService workers = Executors.newFixedThreadPool(2);
CountDownLatch arrived = new CountDownLatch(2);
CountDownLatch phaseOne = new CountDownLatch(1);
CountDownLatch mayLeave = new CountDownLatch(1);
try {
Future<Integer> continuing = workers.submit(() -> {
int current = phaser.arrive();
arrived.countDown();
int next = phaser.awaitAdvanceInterruptibly(current, 3, TimeUnit.SECONDS);
phaseOne.countDown();
await(mayLeave);
phaser.arriveAndDeregister();
return next;
});
Future<Integer> leaving = workers.submit(() -> {
int current = phaser.arriveAndDeregister();
arrived.countDown();
return current;
});
await(arrived);
System.out.println("before coordinator: phase=" + phaser.getPhase()
+ ", parties=" + phaser.getRegisteredParties()
+ ", unarrived=" + phaser.getUnarrivedParties());
int advanced = phaser.arriveAndAwaitAdvance();
await(phaseOne);
System.out.println("after coordinator: phase=" + advanced
+ ", parties=" + phaser.getRegisteredParties());
mayLeave.countDown();
System.out.println("continuing saw=" + continuing.get(3, TimeUnit.SECONDS));
System.out.println("leaving arrived in=" + leaving.get(3, TimeUnit.SECONDS));
phaser.arriveAndDeregister();
System.out.println("terminated=" + phaser.isTerminated());
System.out.println("register after termination is negative=" + (phaser.register() < 0));
} finally {
mayLeave.countDown();
phaser.forceTermination();
workers.shutdownNow();
if (!workers.awaitTermination(3, TimeUnit.SECONDS)) throw new IllegalStateException("workers active");
}
}
}
before coordinator: phase=0, parties=2, unarrived=1
after coordinator: phase=1, parties=2
continuing saw=1
leaving arrived in=0
terminated=true
register after termination is negative=true
开始有三名注册参与者:协调者和两个工作者。离开的工作者完成当前到达并注销,另一名工作者已经到达并等待,所以观察到注册数二、未到达数一。缺少的正是协调者,不是一个神秘丢失的工作线程。
协调者到达后,剩下的两名参与者进入阶段一。继续工作者获准退出以后,协调者最后也注销,注册数变成零,默认 Phaser 随之终止。终止后 register 返回负值,不能靠再注册一名工作者把这份对象复活成新的一轮协调器。
arrive 与 await 是两个动作
arrive() 只登记当前阶段的到达,不等待别人;返回的是到达时的阶段号。awaitAdvance 根据这个阶段号等待变化,却不会替调用者补上一次 arrive。若只 await、不 arrive,可能就是让其他人一直等自己。
程序中 continuing 先 arrive,再使用可中断、带上限的等待。协调者用 arriveAndAwaitAdvance 合并两步,因为在它调用之前,两个工作者的到达已经由独立门闩确认。通用业务若无法确认对方一定到达,就应另行设计超时或终止,而不是无限等待。
还要区分等待上限与参与者注销。某个线程等待超时或被中断,不会自动让它从 Phaser 中离开;其他参与者可能继续等待它后续阶段的到达。异常路径应由协议决定注销还是终止整个计算,不能只捕获异常并让线程悄悄返回。
Phaser 不记录每个人的身份
注册数量和实际任务必须由调用方保持一致。一个线程重复 arrive,Phaser 不会用线程 ID 判断“你已经签过到”,而是可能破坏当前轮次计数;一个已经注销的任务继续随意 arrive,也不符合它的使用合同。
因此不要让业务方法在多个 finally 里无条件 arriveAndDeregister。如果某条路径已经完成本轮到达,清理时再注销可能表达了另一轮动作。可以让每个角色集中管理自己的注册寿命,明确什么时候持有一份参与资格、什么时候已经交回。
动态注册同样不是“任务提交了就自动加入”。注册与实际启动之间若失败,需要补偿那份资格;反过来,先把任务排进执行器,再让它自己决定注册,协调者可能在它还没运行之前就推进阶段。程序选择预先 bulkRegister,阶段零便知道自己究竟等谁。
计数适合观察,不适合当并发裁判
getRegisteredParties、getUnarrivedParties 和 getPhase 在系统运行时会变化。读取三个值不自动构成同一个原子快照;本例使用门闩把工作者暂停,才让打印具有可以重算的含义。真实监控可以展示趋势,但不应通过一次“未到达数等于一”就决定谁可以替别人签退。
forceTermination 会让等待阶段推进的线程看到终止状态,却不等于中断所有工作线程。代码最后仍然关闭执行器,并释放独立门闩。任务若还在等待网络、文件或别的锁,需要相应取消机制;一个同步器结束,不能代替所有资源的寿命管理。
如果每轮参与者固定,可以选用更直接的屏障工具;若只是等待一批任务结束,一次性门闩也许更容易说明。Phaser 的价值在于多轮与参与者增减,但这也要求每条退出路径都能解释它交回的是哪一份资格。先画清“本轮到达”和“以后不再参加”两个动作,往往就能找出阶段为什么没有推进。