文章目录

零、引入
“完了完了,外卖订单系统又卡死了!” 王二拍着键盘哀嚎,他写的 “商家接单” 模块刚上线两小时,服务器 CPU 直接干到 100%,风扇转得像直升机,订单堆了几百条没人处理。领导抱着胳膊站在他身后,脸色比锅底还黑:“再搞不定,今晚你就抱着服务器过通宵!”
王二的代码里,消费者线程循环查队列有没有新订单,没订单就空转;生产者线程加了 synchronized,结果消费者抢不到锁还一直跑,直接把 CPU 跑满。
隔壁哇哥端着枸杞茶晃过来,扫了眼代码乐了:“你这是没学会线程的‘沟通技巧’—— 用 wait () 让线程‘歇着等’,用 notify () 喊它‘干活’,俩方法一配合,CPU 立马降下来。今天给你讲透,不仅救你下班,下次面试被问,你能把面试官说懵。”
点赞 + 关注,跟着哇哥和王二,用食堂打饭的例子搞懂 wait/notify,代码直接抄,线程问题再也不用慌!
一、先破案:王二的 “自杀式” 代码,CPU 为啥炸了?

王二的需求很简单:生产者线程接收用户订单,放进队列;消费者线程从队列里取订单,传给商家。他给队列加了 synchronized 保证线程安全,结果反而把 CPU 干崩了。
👉 王二的 “空转” 代码(必踩坑版)
package cn.tcmeta.threads;
import java.util.LinkedList;
import java.util.Queue;
import static java.lang.Thread.sleep;
/**
* @author: laoren
* @description: // 外卖订单系统——CPU爆炸版
* @version: 1.0.0
*/
public class TakeoutSystemBug {
// 订单队列
static final Queue<String> orderQueue = new LinkedList<>();
// 生产者:接收用户订单(模拟10个订单)
static class Producer implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (orderQueue) { // 加锁保证线程安全
String order = "订单" + i + ":鱼香肉丝盖饭";
orderQueue.add(order);
System.out.println(Thread.currentThread().getName() + ":新增订单→" + order);
}
try {
sleep(1000); // 模拟用户下单间隔
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者:给商家派单(王二的坑在这)
static class Consumer implements Runnable {
@Override
public void run() {
while (true) { // 无限循环查订单
synchronized (orderQueue) {
if (!orderQueue.isEmpty()) {
// 有订单就派单
String order = orderQueue.poll();
System.out.println(Thread.currentThread().getName() + ":派单成功→" + order);
}
// 没订单就空转!CPU杀手就在这
}
}
}
}
static void main() {
// 启动1个生产者,2个消费者
new Thread(new Producer(), "用户下单线程").start();
new Thread(new Consumer(), "商家派单线程1").start();
new Thread(new Consumer(), "商家派单线程2").start();
}
}
运行结果:CPU 直接飙满

控制台里,没订单的时候,两个消费者线程就在synchronized块里无限循环 —— 就算没活干,也一直抢锁、判断、抢锁,把 CPU 资源全占了。用任务管理器一看,Java 进程的 CPU 使用率直奔 100%,服务器不卡死才怪!
“这就像食堂阿姨没饭了还一直喊‘下一个’,” 哇哥吐槽,“食客(线程)没饭吃也不离开,就在窗口前挤着,活活把通道堵死。这时候得让阿姨喊‘没饭了先去候餐区等’(wait ()),有饭了再喊‘来取餐’(notify ())。”
二、用食堂打饭,讲透 wait () 和 notify () 的核心逻辑
哇哥拉过一张纸,画了个食堂窗口,给王二讲明白 wait/notify 到底是啥:
📌 核心比喻:synchronized 是 “窗口锁”,wait/notify 是 “候餐区”
- synchronized:食堂打饭窗口的门锁,只有拿到钥匙(锁)的人才能靠近窗口操作;
- wait():线程拿到锁后,发现没活干(没订单 / 没饭),主动把钥匙放在窗口,自己去候餐区等着,不再抢锁;
- notify():生产者把订单放进队列(食堂做好饭),拿到锁后喊一声 “有活了”,从候餐区叫一个线程过来拿钥匙干活;
- notifyAll():喊一声 “所有人都过来”,候餐区的线程全来抢钥匙(慎用,容易造成拥挤)。
👉 关键规则(王二记满 3 页纸)
哇哥强调,这 3 条规则少一条都要出问题,必须刻进 DNA:
- 必须在 synchronized 块里调用:wait 和 notify 得先拿到 “窗口锁” 才有资格用,不然直接抛弃
IllegalMonitorStateException(没锁还想喊人?门都没有); - wait () 会释放锁:这是核心!线程喊 “我等会儿” 的同时,会把锁交出去,不会占着茅坑不拉屎;
- notify () 不释放锁:喊完 “有活了” 之后,要等当前synchronized块执行完,才会把锁交出去。
三、改造代码:用 wait/notify 让 CPU “凉下来”

哇哥手把手教王二改代码,就加了 3 行:消费者没订单就 wait (),生产者加完订单就 notify (),CPU 直接从 100% 降到 5%。
✔️ 修复后的订单系统(CPU 稳如狗)
package cn.tcmeta.threads;
import java.util.LinkedList;
import java.util.Queue;
/**
* @author: laoren
* @description: 用 wait/notify 让 CPU “凉下来”
* @version: 1.0.0
*/
public class TakeoutSystemFixed {
static final Queue<String> orderQueue = new LinkedList<>();
static class Producer implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
synchronized (orderQueue) {
String order = "订单" + i + ":鱼香肉丝盖饭";
orderQueue.add(order);
System.out.println(Thread.currentThread().getName() + ":新增订单→" + order);
// 核心1:加完订单,喊一个消费者来干活
orderQueue.notify(); // 唤醒一个等待的消费者线程
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (orderQueue) {
// 核心2:没订单就去候餐区等,释放锁
while (orderQueue.isEmpty()) { // 这里必须用while,后面讲原因
try {
System.out.println(Thread.currentThread().getName() + ":暂无订单,等待中...");
orderQueue.wait(); // 释放锁,进入等待状态
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// 被唤醒后,有订单了就派单
String order = orderQueue.poll();
System.out.println(Thread.currentThread().getName() + ":派单成功→" + order);
}
// 派单后休息500ms,模拟商家处理时间
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果: 线程(按需干活、CPU凉透了
用户下单线程:新增订单→订单1:鱼香肉丝盖饭
商家派单线程1:派单成功→订单1:鱼香肉丝盖饭
商家派单线程2:暂无订单,等待中...
商家派单线程1:暂无订单,等待中...
用户下单线程:新增订单→订单2:鱼香肉丝盖饭
商家派单线程2:派单成功→订单2:鱼香肉丝盖饭
商家派单线程1:暂无订单,等待中...
王二看着任务管理器里的 CPU 使用率,激动得拍桌子:“真降到 5% 了!原来线程真的会‘歇着等’,不是一直瞎跑!”
📢 关键改造点拆解(哇哥划重点)

- 生产者加 notify ():新增订单后,唤醒一个等待的消费者 —— 不用喊所有人,避免 “哄抢”;
- 消费者用 while 判断空队列:不能用 if!比如两个消费者都在等,notify () 唤醒一个,另一个如果用 if 会直接往下走,导致取到空订单(这就是 “虚假唤醒”,后面细讲);
- wait () 释放锁:消费者等待时,锁会交出去,生产者能顺利加订单,不会出现 “消费者占着锁空转” 的情况。
四、避坑指南:90% 的人栽在这 3 个问题上
王二照着代码改的时候,又踩了几个坑,哇哥干脆把常见问题整理成 “避坑手册”。
❌ 坑 1:没加 synchronized 就调用 wait/notify,直接报错
// 错误代码:没锁就喊人,必抛异常
public static void main(String[] args) throws InterruptedException {
Queue<String> queue = new LinkedList<>();
queue.wait(); // 运行直接抛IllegalMonitorStateException
}
原因: wait/notify 是 “锁的工具”,没拿到锁就用,相当于没钥匙还想操作窗口,JVM 直接拒接。
❌ 坑 2:用 if 判断空队列,遭遇 “虚假唤醒”

“虚假唤醒” 就是:线程被唤醒后,条件已经变了(比如订单被别的线程取走了),如果用 if 判断,会直接执行后面的代码,导致错误。
package cn.tcmeta.threads;
import java.util.LinkedList;
import java.util.Queue;
/**
* @author: laoren
* @description: 虚假唤醒复现代码(必看)
* @version: 1.0.0
*/
public class SpuriousWakeupSample {
public static final Queue<String> orderQueue = new LinkedList<>();
static void main() {
// 1个生产者,3个消费者
new Thread(() -> {
synchronized (orderQueue) {
orderQueue.add("紧急订单:宫保鸡丁");
System.out.println("生产者:新增紧急订单");
orderQueue.notifyAll(); // 唤醒所有等待的消费者
}
}, "紧急下单线程").start();
// 3个消费者,用if判断空队列
for (int i = 1; i <= 3; i++) {
int finalI = i;
new Thread(() -> {
synchronized (orderQueue) {
if (orderQueue.isEmpty()) { // 坑:用if而不是while
try {
System.out.println("消费者" + finalI + ":等订单...");
orderQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 虚假唤醒:没订单也会执行这里
String order = orderQueue.poll();
System.out.println("消费者" + finalI + ":拿到订单→" + order);
}
}, "消费者" + finalI).start();
}
}
}
执行结果:
生产者:新增紧急订单
消费者1:拿到订单→紧急订单:宫保鸡丁
消费者2:等订单...
消费者3:等订单...
原因:notifyAll () 唤醒了 3 个消费者,只有 1 个拿到订单,另外 2 个被唤醒后,if 判断已经走过了,直接取订单就拿到 null。
**解决方法:**把 if 改成 while,被唤醒后再判断一次条件 —— 没订单就继续等:
while (orderQueue.isEmpty()) { // 用while,唤醒后再检查
orderQueue.wait();
}
❌ 坑 3:notify () 和 notifyAll () 用混,导致性能浪费
notify () 唤醒一个线程,notifyAll () 唤醒所有等待的线程。王二一开始用 notifyAll (),结果每次加订单都唤醒所有消费者,线程抢锁打架,性能反而下降。
用 notify ():一个生产者对应多个消费者,或者任务只能被一个线程处理(比如订单派单);
用 notifyAll ():多个生产者多个消费者,或者需要唤醒所有线程(比如系统停机时,唤醒所有等待线程退出)
五、wait/notify 的经典使用场景(看完就能用)

哇哥给王二讲了两个工作中必用的场景,代码直接抄。
➡️ 场景 1:生产者消费者模型(最常用)
就是前面的外卖订单系统,核心是 “生产后唤醒消费,消费完等待生产”,适合消息队列、订单处理等场景。
📌 场景 2:线程间通信(比如 “主线程等子线程完成”)
王二之前用 Thread.sleep () 等子线程,时间设短了子线程没完成,设长了浪费时间 —— 用 wait/notify 能精准等待。
// 主线程等待子线程完成任务
public class ThreadCommunicationDemo {
// 用一个对象当锁,也可以用当前类的class对象
static final Object lock = new Object();
// 子线程任务是否完成的标记
static boolean taskDone = false;
public static void main(String[] args) throws InterruptedException {
// 子线程:模拟耗时任务(下载文件)
Thread subThread = new Thread(() -> {
synchronized (lock) {
System.out.println("子线程:开始下载文件...");
try {
Thread.sleep(3000); // 模拟3秒下载
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程:文件下载完成!");
taskDone = true;
lock.notify(); // 告诉主线程:我干完了
}
}, "下载线程");
subThread.start();
// 主线程:等待子线程完成
synchronized (lock) {
while (!taskDone) {
System.out.println("主线程:等子线程下载完成...");
lock.wait(); // 释放锁,等待子线程唤醒
}
System.out.println("主线程:拿到下载文件,开始后续处理!");
}
}
}
执行结果:
主线程:等子线程下载完成...
子线程:开始下载文件...
子线程:文件下载完成!
主线程:拿到下载文件,开始后续处理!
六、总结:wait/notify 的核心心法(王二记成顺口溜 - 背下来)
📢 不外传口诀

- 锁内调用是前提:wait/notify 必须套在 synchronized 里;
- wait 放 while 里防假醒:空队列判断用 while,别用 if;
- notify 唤醒一个够:非必要不用 notifyAll,避免线程打架;
- wait 释放锁,notify 不释放:记牢锁的归属,不然全白瞎。
✨ 哇哥的血泪彩蛋

“我刚工作时,把 notify () 写在了 synchronized 外面,” 哇哥捂脸,“结果代码跑起来全是异常,查了俩小时才发现 —— 后来我写 wait/notify,必先检查三点:有锁吗?是 while 吗?notify 在锁里吗?这三点一对照,99% 的坑都能避开。”
🫦 最后说句实在的

wait/notify 是 Java 线程通信的 “老伙计”,虽然现在有 Condition 比它更灵活,但面试必问,老项目里也经常见。记住:它的核心是让线程 “按需干活”,别空转浪费资源—— 就像食堂阿姨合理安排食客,既不堵门,也不耽误吃饭。
今天的代码你复制过去,改改业务逻辑就能用在订单系统、文件处理、线程通信里。如果这篇帮你解决了 CPU 爆满的问题,点赞 + 分享给你那还在写 “空转线程” 的同事!
关注我,下次咱们扒一扒 wait/notify 和 Condition 的区别 —— 为什么说 Condition 是 “升级版”?怎么用它实现精准唤醒?让你不仅会用老工具,还能玩转新特性,面试直接碾压面试官!
用wait/notify解决CPU空转

被折叠的 条评论
为什么被折叠?



