深蓝黑背景封面:右侧大标题「线程协作,该用哪件工具」,副标题「wait/notify、Condition、Semaphore、Phaser 的适用场景」;左侧原创示意图——一只打开的工具箱,四格分别放着四件工具:一把挂着条件标签的旧式铜锁(wait/notify)、一把带多把钥匙孔的现代锁(Condition)、一叠许可证卡片(Semaphore)、一只分段汇合的进度旗(Phaser);左上标注「知识专栏 · 并发协作」,右上标注「Day 09 · 11-06」 图 1:本文封面,先认清协作模式,工具自动对应。

「讲讲线程的生命周期」是道经典面试题,那道题的主轴是状态机——NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED、TERMINATED 之间怎么迁移,wait、sleep、join 各自把线程推到哪里。真到写代码,问题换了形状:几个线程要配合完成一件事,等条件、限并发、分阶段汇合,分别该抄哪件工具?本文把 JDK 官方文档里的语义核验过后,按协作模式把工具箱讲一遍。

模式一:条件等待——「等某个条件成立再继续」。

最古老的原语是 Object 的 wait/notify,必须配 synchronized 使用:wait 原子地释放锁并挂起,被唤醒后要重新抢到锁才能返回。它有两个经典坑。其一,一个监视器只配一个等待队列——生产者和消费者挤在同一个队列里,notify 随机挑一个唤醒,可能唤起一个同类角色,等于白唤,所以稳妥写法只能是 notifyAll,全员唤起再重新竞争,JDK 文档对 notify 的措辞就是「唤醒任一等待线程」,不保证是谁。其二,被唤醒不等于条件成立——JLS 第 17 章明确允许虚假唤醒(spurious wakeup),所以等待必须写在 while 循环里,醒来先复查条件。

Condition 是这个模式的现代答案。它配 Lock 使用,官方文档写得很直白:Condition 把 Object 的监视器方法拆解成独立对象,让一把锁可以挂多个等待队列。有界缓冲区里 notFull 和 notEmpty 各是一个 Condition,put 满了在 notFull 上 await,生产完对 notEmpty 调 signal——只唤消费者队列,不产生无效唤醒。从 wait/notify 迁移到 Condition,等价于把「一把锁一个队」升级成「一把锁多个队」。

模式二:许可证——「同时最多放行 N 个」。

Semaphore 维护一组许可:acquire 拿不到就阻塞等待,release 归还一个许可。它天然是连接池、限并发闸门的形状——池子里 10 个连接就发 10 张许可,线程凭许可取用,用完归还。官方文档点出它与锁的本质差别:许可的归还没有所有权的概念,可以由完全不同的线程 release(锁必须由持有者释放),这让它能做跨线程的移交,比如死锁恢复场景。构造时可以选公平策略:公平模式按 FIFO 发许可防饿死,非公平允许插队换吞吐。

模式三:分阶段汇合——「大家到齐,一起进入下一阶段」。

日常最常用的是 CountDownLatch(一次性倒数)与 CyclicBarrier(可复用汇合点),这两个本文只点名不展开。这里的主菜是 Phaser:官方文档称它为「可复用的同步屏障,功能类似 CyclicBarrier 与 CountDownLatch 但用法更灵活」。灵活在两处:参与方数量可以动态变化,任务随时 register 加入、arriveAndDeregister 退出;阶段是显式编号的,所有注册方到齐后 phase 自动加一,可以反复汇合多轮。并行算法里「第一阶段全员算完再进第二阶段」的多轮迭代,就是它的主场。

四种模式跑一遍。

下面这份 JDK 17 实测代码把四件工具各用一个最小场景演示,直接编译运行(javac CoordinationDemo.java && java CoordinationDemo):

// 模式一:wait/notify 版有界缓冲区——一把锁一个队列,只能 notifyAll
synchronized void put() throws InterruptedException {
    while (count == cap) wait(); // 必须在循环里等:被唤醒 ≠ 条件成立
    count++;
    notifyAll(); // 单队列分不清该唤谁,全员唤起再竞争
}

// 模式一升级版:Condition 版——一把锁挂两个条件队列,精确唤醒
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
void put() throws InterruptedException {
    lock.lock();
    try {
        while (count == cap) notFull.await();
        count++;
        notEmpty.signal(); // 只唤消费者队列
    } finally { lock.unlock(); }
}

// 模式二:Semaphore 管理 2 张许可,4 个线程排队取用
Semaphore permits = new Semaphore(2);
permits.acquire();  // 拿不到就排队
try { /* 使用资源 */ } finally { permits.release(); } // 许可可跨线程归还

// 模式三:Phaser 多阶段汇合,W0 只做一阶段就动态退出
phaser.register();            // 动态注册参与者
phaser.arriveAndAwaitAdvance(); // 等全员到齐,phase 加一
phaser.arriveAndDeregister();   // 中途退出

实测输出(线程交错顺序每次运行略有不同,语义不变):

== 条件等待:wait/notify(单队列) ==
  [wait/notify] 生产 -> count=1
  [wait/notify] 消费 -> count=0
== 条件等待:Condition(双队列精确唤醒) ==
  [Condition] 生产 -> count=1
  [Condition] 消费 -> count=0
== 许可证:Semaphore ==
  [Semaphore] T0 等待许可(剩 2)
  [Semaphore] T0 拿到许可
  [Semaphore] T2 拿到许可
  [Semaphore] T0 归还许可
== 分阶段汇合:Phaser ==
  [Phaser] W0 完成阶段0
  [Phaser] W1 完成阶段0
  [Phaser] 阶段0全员到齐,进入阶段1
  [Phaser] W1 完成阶段1(phase=1)

选型谱系图:上方按协作模式分三类——条件等待、许可证发放、分阶段汇合,每类挂对应工具。条件等待类:wait/notify 配 synchronized,一个条件队列,notify 随机唤醒容易弄错对象;Condition 配 Lock,可挂多个条件队列精确唤醒,生产者与消费者各排各的队。许可证类:Semaphore 管理固定数量的许可,拿不到就排队,天然做连接池与限并发闸门;发许可与还许可可以跨线程,这是它与锁的本质差别。汇合类:Phaser 支持多阶段与动态注册,参与者到齐进下一阶段,可中途加入退出;CountDownLatch 与 CyclicBarrier 是其特例。底部提示:先描述协作模式——「等某个条件」「限同时进入数」「大家到齐再走」,工具自动对应 图 2:模式先行,工具是模式的名字。

选型口诀与工程落点。

把需求翻译成一句话,工具就自己站出来:「等某个条件成立」用条件等待,新代码一律 Condition 而不是 wait/notify;「同时最多 N 个」用 Semaphore;「大家到齐再走」用汇合工具,一次性倒数 CountDownLatch、固定人数多轮 CyclicBarrier、人数会变动或多阶段 Phaser。AI 工程里这些模式天天出现:推理实例池就是 Semaphore,流水线各阶段对齐就是 Phaser,队列生产者消费者就是 Condition。选对原语不只是省事——错误唤醒、许可泄漏、汇合点僵死这类并发 bug,几乎都是「模式和工具不匹配」的代价。

你项目里的线程协作用的是哪几件?有没有遇到过 wait/notify 时代遗留代码迁到 Condition 的契机?欢迎聊聊。