用 wait () 做点餐系统被骂后,Condition 教我给线程 “精准喊号”!
零、引入
你刚用 wait() 实现了外卖催单功能,信心爆棚,主动接下公司食堂的“智能点餐系统”开发任务,信誓旦旦地表示“线程协作稳如老狗”。可系统上线第一天就翻车:堂食顾客点的“番茄炒蛋”等了20分钟还没上桌,反倒是外卖订单接连不断被优先处理。顾客怒拍桌子,直呼“系统歧视堂食”,领导更是把你叫进办公室,拍桌声堪比 JVM 的 GC 回收:“3小时内修好,否则这个月全勤奖泡汤!”
你急得满头大汗,排查代码才发现问题所在:使用 wait() 和 notifyAll() 实现的订单唤醒机制,无论是堂食还是外卖订单,一旦唤醒,所有线程都挤到厨师面前,厨师随机抓取一个处理,毫无秩序可言。就在你准备把“离职申请.pdf”重命名为“全勤奖申诉.pdf”时,王哥叼着煎饼果子走过来,边嚼边说:“慌啥?wait() 这种‘广播喇叭’根本搞不定多条件等待!该用 Condition 给线程‘分群喊号’,比你这‘瞎唤醒’强十倍!”
一、先吐槽 wait ():多条件场景下就是 “添乱神器”
王哥抹了抹嘴角的葱花,指着你的代码笑出皱纹:“你把堂食和外卖订单放在同一个队列里,共用一把锁,还用同一个 wait(),就像餐厅只有一个广播喇叭——不管是谁的餐好了,全都喊‘有人取餐’。结果堂食客人跑过去发现是外卖单,外卖小哥赶来一看又是堂食,这不是乱套了吗?”
2.1 先搞懂 Condition 的核心优势(对比 wait ())
wait() 的问题是“一刀切”:它只能在单一条件下等待,且 notifyAll() 会唤醒所有等待线程,导致不必要的竞争与判断。而 Condition 提供了更细粒度的控制能力——一个锁可以绑定多个 Condition 实例,每个 Condition 可以独立 await() 和 signal(),实现“按组通信”。
换句话说,Condition 就像给不同类型的订单配备了专属对讲机:堂食订单用 A 频道,外卖订单用 B 频道。厨师想做堂食时,只呼叫 A 频道;外卖来了,才触发 B 频道通知,彻底避免无效唤醒。
2.2 示例代码
public class GoodCanteenDemo {
// 使用 ReentrantLock 替代 synchronized
static final ReentrantLock lock = new ReentrantLock();
// 为堂食订单创建独立 Condition
static final Condition dineInCond = lock.newCondition();
// 为外卖订单创建独立 Condition
static final Condition takeoutCond = lock.newCondition();
// 堂食订单队列
static Queue<String> dineInOrders = new LinkedList<>();
// 外卖订单队列
static Queue<String> takeoutOrders = new LinkedList<>();
public static void main(String[] args) {
// 厨师线程:负责处理订单
new Thread(() -> {
while (true) {
lock.lock();
try {
// 优先处理外卖订单(假设业务如此)
while (takeoutOrders.isEmpty()) {
if (!dineInOrders.isEmpty()) break;
System.out.println("厨师:暂无订单,歇会儿...");
takeoutCond.await(); // 等待外卖订单
}
// 先做外卖
if (!takeoutOrders.isEmpty()) {
String order = takeoutOrders.poll();
System.out.println("厨师:做外卖订单——" + order);
}
// 再看堂食
else if (!dineInOrders.isEmpty()) {
String order = dineInOrders.poll();
System.out.println("厨师:做堂食订单——" + order);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "厨师").start();
// 堂食下单线程(顾客1)
new Thread(() -> {
lock.lock();
try {
dineInOrders.add("番茄炒蛋+米饭");
System.out.println("顾客1:点了堂食——番茄炒蛋+米饭");
dineInCond.signal(); // 只通知堂食相关的等待者
} finally {
lock.unlock();
}
}, "堂食顾客1").start();
// 外卖下单线程(外卖员1)
new Thread(() -> {
lock.lock();
try {
takeoutOrders.add("宫保鸡丁+饮料");
System.out.println("外卖员1:提交了外卖订单——宫保鸡丁+饮料");
takeoutCond.signal(); // 只唤醒外卖处理逻辑
} finally {
lock.unlock();
}
}, "外卖员1").start();
}
}
三、wait () vs Condition:核心区别大对比
| 对比维度 | wait()/notify() | Condition |
|---|---|---|
| 底层锁机制 | 依赖 synchronized | 配合 Lock 使用 |
| 等待队列数量 | 每对象仅一个等待队列 | 一个 Lock 可创建多个 Condition,对应多个等待队列 |
| 唤醒精度 | notify()/notifyAll() 全体唤醒,易造成“虚假唤醒” | signal()/signalAll() 可针对特定 Condition 唤醒,精准调度 |
| 灵活性 | 低,无法自定义等待策略 | 高,支持中断等待、超时等待、公平锁集成等 |
四、使用场景:什么时候用 wait (),什么时候用 Condition?
4.1 用 wait () 的场景:简单单条件等待
当系统中只有一个等待条件,且并发量不高时,使用 wait() 完全够用。例如:
- 生产者-消费者模型中的基础版本(只有一个缓冲区)
- 简单的资源池等待(如数据库连接池初始化完成前阻塞)
这类场景无需复杂控制,synchronized + wait/notify 已足够清晰简洁。
4.2 用 Condition 的场景:多条件分组等待
当存在多种等待状态或需要分类通知时,必须使用 Condition。典型例子包括:
- 订单系统中区分堂食、外卖、预约订单的处理
- 任务调度器中按优先级划分 High/Medium/Low 队列
- 读写锁内部实现中 Read 和 Write 的独立等待机制
此时若强行使用 wait(),将导致大量线程被唤醒后又因条件不符重新进入等待,浪费 CPU 资源。
五、总结:3 句口诀 + 避坑指南
- 单条件用 wait,多条件用 Condition —— 条件越多,越要分通道通信。
- 广播唤醒是隐患,精准喊号才高效 —— notifyAll() 是“广而告之”,容易引发惊群效应。
- Lock 配 Condition,灵活掌控线程流 —— 更高级的同步控制离不开组合拳。
5.1 避坑指南(王哥的血泪史)
- 别忘了 unlock():使用 Lock 必须确保 finally 中释放锁,否则死锁风险极高。
- signal 要及时:订单加入后必须立即调用对应 Condition.signal(),否则线程可能永久挂起。
- while 判断不能改成 if:await() 返回后仍需重新检查条件,防止虚假唤醒或信号丢失。
- 不要混用 synchronized 与 Condition:它们属于不同体系,混合使用会导致行为不可预测。
六、最后说句实在的
早期 Java 并发编程依赖 wait()/notify() 是无奈之举,但如今有了更强大的工具,就不该再拿“老古董”硬扛复杂场景。就像王哥最后撂下的一句话:“你以为你在写代码?其实你是在设计流程。流程不清,代码再漂亮也是垃圾。”
从“广播喇叭”升级到“分群对讲机”,不只是技术选型的变化,更是对并发逻辑理解的跃迁。下次遇到多条件等待,别再拍脑袋用 wait() 了,试试 Condition,让线程真正“听懂指令”。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class GoodCanteenDemo {
// 可重入锁:替代 synchronized,功能更强大
static final Lock LOCK = new ReentrantLock();
// 堂食专属 Condition(对讲机1):用于堂食订单的等待与唤醒
static final Condition DINE_IN_COND = LOCK.newCondition();
// 外卖专属 Condition(对讲机2):用于外卖订单的等待与唤醒
static final Condition TAKEOUT_COND = LOCK.newCondition();
// 堂食订单队列
static Queue<String> dineInOrders = new LinkedList<>();
// 外卖订单队列
static Queue<String> takeoutOrders = new LinkedList<>();
public static void main(String[] args) {
// 厨师线程:现在能精准处理不同类型的订单
new Thread(() -> {
while (true) {
LOCK.lock();
try {
// 优先检查堂食订单
while (dineInOrders.isEmpty()) {
System.out.println("厨师:暂无堂食订单,等堂食顾客下单...");
DINE_IN_COND.await(); // 仅等待堂食通知
}
// 处理当前堂食订单
String dineInOrder = dineInOrders.poll();
System.out.println("厨师:做堂食订单——" + dineInOrder);
// 若堂食处理完还有空闲,再检查外卖订单
while (takeoutOrders.isEmpty()) {
System.out.println("厨师:暂无外卖订单,等外卖订单来...");
TAKEOUT_COND.await(); // 仅等待外卖通知
}
// 处理当前外卖订单
String takeoutOrder = takeoutOrders.poll();
System.out.println("厨师:做外卖订单——" + takeoutOrder);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
LOCK.unlock();
}
}
}, "厨师").start();
// 顾客1:点堂食
new Thread(() -> {
LOCK.lock();
try {
dineInOrders.add("番茄炒蛋+米饭");
System.out.println("顾客1:点了堂食——番茄炒蛋+米饭");
DINE_IN_COND.signal(); // 只通知堂食相关的线程
} finally {
LOCK.unlock();
}
}, "顾客1").start();
// 外卖员1:提交外卖订单
new Thread(() -> {
LOCK.lock();
try {
takeoutOrders.add("黄焖鸡米饭+可乐");
System.out.println("外卖员1:点了外卖——黄焖鸡米饭+可乐");
TAKEOUT_COND.signal(); // 只唤醒外卖处理线程
} finally {
LOCK.unlock();
}
}, "外卖员1").start();
}
}
跑起来可能出现的日志示例:
厨师:暂无堂食订单,等堂食顾客下单...
顾客1:点了堂食——番茄炒蛋+米饭
厨师:做堂食订单——番茄炒蛋+米饭
厨师:暂无外卖订单,等外卖订单来...
外卖员1:点了外卖——黄焖鸡米饭+可乐
厨师:做外卖订单——黄焖鸡米饭+可乐
“你看,”王哥说道,“原本使用 wait() 和 notifyAll() 的时候,所有线程都挤在一个等待队列里,一旦唤醒就是全员出动,但真正符合条件的可能只有一个。这就像在公司群里喊‘谁有空来帮忙’,结果设计、运维、司机全来了,只有一个人能干活,其余都是无效响应。”
他喝了口茶继续解释:“而 Condition 就像是给每个任务组配了专用对讲机。堂食用一个频道,外卖用另一个。你喊哪个频道,就只有对应的人听到并响应,避免资源浪费和混乱。”
Condition 的核心优势(对比 wait/notify):
- 多条件分组:一个 Lock 可以创建多个 Condition 实例,每个实例维护独立的等待队列。例如,餐厅中可以将堂食和外卖分别管理,互不干扰。
- 精准唤醒:通过 signal() 或 signalAll() 只唤醒指定 Condition 上等待的线程,不会波及其它无关线程,提升效率与可控性。
- 更灵活的控制机制:支持带超时的等待(awaitNanos, awaitUntil)、可中断等待(awaitUninterruptibly 除外),还能查询当前等待队列长度(getWaitQueueLength()),比传统的 wait() 更加丰富和安全。
// 检查并处理外卖订单之前,先完成堂食任务
while (takeoutOrders.isEmpty()) {
System.out.println("厨师:暂无外卖订单,等待外卖员下单...");
TAKEOUT_COND.await(); // 等待外卖通知信号
}
// 开始制作外卖餐品
String takeoutOrder = takeoutOrders.poll();
System.out.println("厨师:正在处理外卖订单——" + takeoutOrder);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock(); // 释放锁,确保异常时也能解锁
}
}, "厨师").start();
// 堂食顾客下单线程(模拟顾客1点餐)
new Thread(() -> {
LOCK.lock();
try {
dineInOrders.add("番茄炒蛋+米饭");
System.out.println("顾客1:提交了堂食订单——番茄炒蛋+米饭");
// 仅唤醒与堂食相关的等待线程
DINE_IN_COND.signal(); // 类似于 notify(),但更精确
} finally {
LOCK.unlock();
}
}, "堂食顾客1").start();
// 外卖员提交订单线程(模拟外卖员1发送订单)
new Thread(() -> {
LOCK.lock();
try {
takeoutOrders.add("黄焖鸡米饭+可乐");
System.out.println("外卖员1:提交了外卖订单——黄焖鸡米饭+可乐");
// 仅通知处理外卖的线程
TAKEOUT_COND.signal(); // 精准唤醒外卖处理逻辑
} finally {
LOCK.unlock();
}
}, "外卖员1").start();
}
}
运行结果(顺序清晰、执行精准):
厨师:暂无堂食订单,正在等待顾客下单...
顾客1:提交了堂食订单——番茄炒蛋+米饭
厨师:正在处理堂食订单——番茄炒蛋+米饭
厨师:暂无外卖订单,等待外卖员下单...
外卖员1:提交了外卖订单——黄焖鸡米饭+可乐
厨师:正在处理外卖订单——黄焖鸡米饭+可乐
“看,现在是先来的堂食先做,后到的外卖后处理,完全不混乱!”王哥笑着说,“Condition 就像是给不同类型的线程划分了独立的工作组,每组都有自己的通信频道。通知的时候只叫对应组的人,其他人不受影响——这比 wait() 那种‘全公司广播’的方式高效太多了。”
三、wait() 与 Condition 的核心差异对比
生活化类比:
wait():
想象学校只有一个公共广播系统,老师说“请班长来办公室”,于是全校所有班级的班长都得跑过去查看,结果发现只找一班班长——其他班长白白浪费时间来回奔波,还耽误本职工作;
Condition:
每个班级配备专用对讲机,老师直接呼叫“一班班长来办公室”,只有目标班长收到消息并行动,其余班级继续上课,互不干扰,效率显著提升。
“再举个程序中的例子,”王哥接着解释道,“假如你设计一个‘生产者-消费者’模型,其中生产者分为‘水果类’和‘蔬菜类’,消费者也分两类。如果使用 wait(),一旦唤醒,可能导致水果消费者误入蔬菜队列,反之亦然——造成资源错配和空等现象;
而使用 Condition,则可以分别为水果和蔬菜创建独立的等待条件,实现按类别精准唤醒,彻底避免交叉干扰。”
四、应用场景解析:何时选择 wait()?何时使用 Condition?
王哥喝了一口冰阔落,语气笃定地总结:
4.1 推荐使用 wait() 的情况:单一条件等待场景
适用于逻辑简单、仅需一个同步条件的情境,例如:
- 外卖订单催单功能(只需等待“接单”这一事件);
- 基础线程协作(如线程A等待线程B完成某项任务后再继续)。
这类场景下,使用 wait() 完全足够,代码简洁,且由 synchronized 自动管理加锁与解锁流程,无需手动控制。
“就像你只需要喊一声‘大家都停下等我’,用一个大喇叭(wait())就够了,没必要每人发一台对讲机。”
4.2 推荐使用 Condition 的情况:多条件分组等待场景
当面临以下复杂需求时,应优先选用 Condition:
- 多类型订单并行处理(如区分堂食与外卖、普通客户与VIP订单);
- 复杂的生产者-消费者结构(多种生产者与消费者,需按商品或任务类型分组);
- 要求精确唤醒特定线程(如高优先级任务优先调度);
- 需要获取当前等待队列的状态信息(例如统计有多少外卖订单正在排队)。
“在这些情况下,若仍使用 wait(),很容易导致线程响应错乱,整个系统像一锅乱炖;而 Condition 相当于为线程建立了清晰的组织架构,分工明确,调度有序——这就是专业工具带来的本质区别。”
插个总结:“很多程序员分不清这俩,就像分不清‘一次性筷子’和‘可降解分餐筷’—— 都是筷子,但使用场景不同:日常吃饭图方便用一次性的(wait()),多人分餐、注重秩序与卫生则用分餐筷(Condition);若用 wait() 来处理多个等待条件,就如同拿一次性筷子给十个人分菜,混乱且不安全!”
五、总结:3 句口诀 + 避坑指南
单条件用 wait(),多条件用 Condition: 场景越简单,越不需要引入复杂的工具。就像小问题不必动用重型机械,基础同步手段足够应对单一等待条件。
Condition 必须搭配 Lock 使用: 不要试图将 Condition 与 synchronized 混用,这就像让对讲机连接广播喇叭,两者协议不匹配,根本无法协同工作。
Lock 必须手动释放: 调用 Condition 的 await() 方法并不会自动释放锁。因此,unlock() 一定要放在 finally 块中执行。否则一旦发生异常,锁将无法释放,导致后续线程全部阻塞——这种情况比在同步块外调用 wait() 还严重得多!
5.1 避坑指南(王哥的血泪史)
“我刚开始用 Condition 的时候,把 unlock() 写在了 try 块里面,” 王哥捂脸回忆道,“结果代码中途抛了异常,unlock() 根本没机会执行,锁一直被占用,所有线程都卡死了。我当时以为系统彻底崩溃,还傻乎乎地重启了服务器,结果被领导狠狠批评‘连个锁都解不开’。”
“从那以后,我写 Condition 相关逻辑时,第一件事就是先写下 lock(),然后立刻补上 finally 块里的 unlock(),最后才填充中间的业务代码。现在这套流程我已经熟得像每天摸鱼一样自然了!”
六、最后说句实在的
Condition 更加专业、灵活。今天分享的这两段代码,你拿过去直接运行就能用,只需根据实际需求调整 Condition 的数量以及唤醒的逻辑,就能轻松集成到自己的项目中。
如果你已经理解透彻,不妨在下次团队协作时帮助他人提升效率。比如看到同事还在用 wait() 处理多个等待条件,导致代码混乱不堪,可以在代码 review 时微笑着说:“兄弟,别再用广播喇叭喊人了,给每个线程配个专属对讲机(Condition),让它们各司其职、精准响应,不香吗?”



雷达卡


京公网安备 11010802022788号







