【030】咋啦? 线程空转 CPU 炸了?王二用 wait/notify 救场,代码抄完直接用!

用wait/notify解决CPU空转

请添加图片描述

零、引入

“完了完了,外卖订单系统又卡死了!” 王二拍着键盘哀嚎,他写的 “商家接单” 模块刚上线两小时,服务器 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 是 “升级版”?怎么用它实现精准唤醒?让你不仅会用老工具,还能玩转新特性,面试直接碾压面试官!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值