先说几个核心判断:Phaser 的真正价值并不局限于“多阶段同步”这一表面特性,而在于它巧妙融合了“动态参与者机制”与“隐式阶段推进”能力。它不依赖事先固定的线程数,而是通过运行时的显式注册、原子退出以及阶段号的自动递增,轻松应对任务规模动态变化、角色分工各异的真实并发场景,堪称 Java 并发工具中的灵活同步利器。

参与者必须显式注册,不能仅靠构造参数“占位”
写入一行 new Phaser(5),仅仅让内部计数器初始化为 5,这并不代表有 5 个合法的参与者真正就位。如果未调用 register() 就直接调用 arriveAndAwaitAdvance(),唯一的结果就是抛出 IllegalStateException: Not a registered party 异常。
那么,正确的安全起点应该怎么做?
- 最稳妥的方式是使用
new Phaser()或new Phaser(null, 0),从零开始逐个注册,做到一个萝卜一个坑。 - 每个工作线程启动后,在执行业务逻辑之前,第一件事就是调用
phaser.register()进行显式注册。 - 如果主线程也需要参与(例如汇总结果),同样必须自己执行
register();如果主线程只负责协调、不参与具体任务,那就不注册,改用awaitAdvance(int)来等待阶段完成。 - 批量注册时可以使用
bulkRegister(n),虽然比循环调用register()效率略高,但它仍然是运行时动态行为,并非声明参与者的数量上限。
阶段性退出必须原子完成,不能拆成两步
当一个线程完成某个阶段后,不再参与后续流程,这时必须使用 arriveAndDeregister() 这一原子操作,一次性完成“到达”与“注销”。很多开发者容易犯的错误是,误以为 deregister() 是一个独立可调用的方法——实际上它根本就不是 public 的。试图分两步先写 arrive() 再调用某个所谓的注销方法,是行不通的。
几个典型场景:
- 预处理线程加载完数据后,立即调用
arriveAndDeregister(),干净利落地退出同步流程。 - 汇总线程需要等待所有子任务结束,所以全程使用
arriveAndAwaitAdvance(),或配合awaitAdvance()进行等待。 - 容易踩坑的写法是:先调用
arriveAndAwaitAdvance(),再调用arriveAndDeregister()。前者已经推进了 phase,后者可能落在新的 phase 上,导致计数错乱。
阶段推进由“到达动作”触发,不是自动发生的
Phaser 的 phase 号不会凭空递增。只有当已注册的参与者调用了 arrive()、arriveAndAwaitAdvance() 或 arriveAndDeregister() 时,内部计数器才会响应。那些尚未到达的线程,是不会推动阶段前进的。
- 每个
arriveAndAwaitAdvance()的含义是:“我到达了,并且我等待其他人完成。” 返回时,phase 已经自动 +1。 arrive()仅完成“到达”动作,不等待;如果后续需要等待,必须搭配awaitAdvance(int)使用。awaitAdvance(int phase)是非阻塞等待的起点。但要注意:调用它之前,必须先执行arrive()或arriveAndDeregister(),否则会永远卡住。
主线程应扮演节奏控制器,而非盲目同步者
主线程通常不参与具体任务,它的角色是控制流程的边界与节奏。因此,它不应和工作线程一样调用 arriveAndAwaitAdvance(),而应该精准地等待某个 phase 结束,再触发下一阶段的操作。
一个优雅的实践做法是:
- 在启动子任务前,先记录当前的 phase:
int p0 = phaser.getPhase()。 - 分发完所有任务后,调用
phaser.awaitAdvance(p0),确保所有任务都抵达了第 0 阶段。 - 然后再启动第 1 阶段的逻辑(例如数据校验),避免出现“部分任务还没开始,主线程已经往下冲”的混乱局面。
- 牢记:
awaitAdvance()不会自动执行arrive(),调用者自己必须先完成到达动作。
