图 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)
图 2:模式先行,工具是模式的名字。
选型口诀与工程落点。
把需求翻译成一句话,工具就自己站出来:「等某个条件成立」用条件等待,新代码一律 Condition 而不是 wait/notify;「同时最多 N 个」用 Semaphore;「大家到齐再走」用汇合工具,一次性倒数 CountDownLatch、固定人数多轮 CyclicBarrier、人数会变动或多阶段 Phaser。AI 工程里这些模式天天出现:推理实例池就是 Semaphore,流水线各阶段对齐就是 Phaser,队列生产者消费者就是 Condition。选对原语不只是省事——错误唤醒、许可泄漏、汇合点僵死这类并发 bug,几乎都是「模式和工具不匹配」的代价。
你项目里的线程协作用的是哪几件?有没有遇到过 wait/notify 时代遗留代码迁到 Condition 的契机?欢迎聊聊。