JavaEE初阶——线程通信:wait和notify的通俗解读
本文摘要:文章详细讲解了Java线程通信中的wait()、notify()和notifyAll()机制。通过生产者-消费者模型示例代码,展示了这些方法的使用场景和注意事项。重点解析了wait()必须在while循环中调用的原因(防止虚假唤醒),以及与sleep()的关键区别(是否释放锁)。文章用生动的"厕所"比喻帮助理解线程协作原理,并指出现代替代方案(Lock和Conditi
目录
1. 完整的 Java 代码演示 (Producer-Consumer 模型)
A. 为什么 wait() 必须在 while 循环中调用?
B. IllegalMonitorStateException
这张图片展示的是 Java 多线程编程 中关于 线程通信(Thread Communication) 的核心知识点大纲,特别是围绕 Object 类提供的 wait() 和 notify() 机制。
根据您的要求,我将编写一段完整的 Java 代码来演示这些概念,逐行注释,然后详细解析图片中的知识点以及 wait 和 sleep 的经典面试题对比。
1. 完整的 Java 代码演示 (Producer-Consumer 模型)
为了演示 wait()、notify() 和 notifyAll(),最经典的场景是生产者-消费者模型。
Java
package com.example.thread;
// 定义一个简单的共享资源类
class SharedResource {
// 这是一个共享的数据变量
private String message;
// 这是一个标志位,true 表示有数据(Wait for consumer),false 表示没数据(Wait for producer)
private boolean hasMessage = false;
// 7.1 & 7.3: 生产数据的方法
public synchronized void produce(String newMessage) {
// 使用 while 循环而不是 if,防止“虚假唤醒”(Spurious Wakeup)
while (hasMessage) {
try {
// 打印当前状态,表示生产者需要等待
System.out.println("生产者: 队列已满,等待消费者消费...");
// 7.1 wait()方法:释放锁,当前线程进入等待状态
wait();
} catch (InterruptedException e) {
// 处理线程中断异常
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
// 模拟生产数据的过程
this.message = newMessage;
this.hasMessage = true;
System.out.println("生产者: 已生产消息 -> " + newMessage);
// 7.2 / 7.3 notify() 或 notifyAll() 方法:唤醒等待的线程
// notify() 随机唤醒一个,notifyAll() 唤醒所有。这里演示 notify()
notify();
}
// 消费数据的方法
public synchronized void consume() {
// 如果没有消息,消费者需要等待
while (!hasMessage) {
try {
// 打印当前状态
System.out.println("消费者: 队列为空,等待生产者生产...");
// 7.1 wait()方法:释放锁,暂停执行
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
// 模拟消费数据的过程
System.out.println("消费者: 已消费消息 -> " + this.message);
this.hasMessage = false;
// 唤醒生产者线程
notify();
}
}
// 测试主类
public class WaitNotifyDemo {
public static void main(String[] args) {
// 创建共享资源对象
SharedResource resource = new SharedResource();
// 创建生产者线程
Thread producerThread = new Thread(() -> {
// 生产 3 条数据
String[] messages = {"消息A", "消息B", "消息C"};
for (String msg : messages) {
resource.produce(msg);
try {
// 模拟生产耗时,sleep 不会释放锁,但这里是在同步块之外调用的
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Producer-Thread");
// 创建消费者线程
Thread consumerThread = new Thread(() -> {
// 消费 3 条数据
for (int i = 0; i < 3; i++) {
resource.consume();
try {
// 模拟消费耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Consumer-Thread");
// 启动线程
producerThread.start();
consumerThread.start();
}
}
2. 图片涉及知识点详细解析
图片中的大纲涵盖了 Java 线程间通信的基础机制。所有这些方法都属于 java.lang.Object 类,而不是 Thread 类,这意味着任何对象都可以作为锁(Monitor)。
7.1 wait() 方法
-
定义: 使当前线程立即停止运行,释放对象锁,并进入等待队列(Wait Set)。
-
前提条件: 调用
wait()的线程必须拥有该对象的监视器锁(Monitor),即必须在synchronized代码块或方法中调用。 -
核心行为:
-
释放锁: 这是与
sleep()最关键的区别。 -
阻塞: 线程进入
WAITING或TIMED_WAITING状态。 -
等待唤醒: 直到其他线程调用同一个对象的
notify()或notifyAll()。
-
7.2 notify() 方法
-
定义: 唤醒在此对象监视器上等待的单个线程。
-
随机性:如果有多个线程在等待,选择唤醒哪一个是任意的(取决于具体的 JVM 实现),开发者无法指定唤醒哪一个。
-
锁的持有: 调用
notify()后,当前线程不会立即释放锁,而是要等到执行完当前的synchronized代码块后才释放。被唤醒的线程也要等到获得锁之后才能继续执行。
7.3 notifyAll() 方法
-
定义: 唤醒在此对象监视器上等待的所有线程。
-
行为: 所有等待的线程都会被唤醒,并开始竞争锁。只有一个线程能抢到锁并执行,其他的线程会再次进入阻塞状态(Blocked),等待锁被释放。
-
使用场景: 当有多个线程等待,且你不确定应该唤醒哪一个,或者多个线程等待的条件可能同时满足时,使用
notifyAll()更安全,可以避免“信号丢失”导致的死锁。
7.4 wait 和 sleep 的对比 (高频面试题)
这是 Java 面试中最常见的问题之一。
| 特性 | Object.wait() | Thread.sleep() |
| 所属类 | Object 类 |
Thread 类 |
| 锁的释放 | 释放锁 (Release Lock) | 不释放锁 (Hold Lock) |
| 使用范围 | 必须在 synchronized 块中 |
任何地方 |
| 用途 | 线程间通信 (Thread Communication) | 暂停执行 (Pause Execution) |
| 唤醒方式 | 需要 notify() / notifyAll() (除非设置超时) |
时间到了自动唤醒 |
| 异常 | 抛出 InterruptedException |
抛出 InterruptedException |
一句话总结:
wait是用来让线程之间协作的(交出控制权和锁),而sleep只是让线程休息一下(不交出锁)。
3. 详细扩展说明
为了深入理解,我们需要补充几个图里没写但非常重要的点:
A. 为什么 wait() 必须在 while 循环中调用?
在上面的代码中,我使用了:
Java
while (condition) {
wait();
}
而不是:
Java
if (condition) {
wait();
}
原因: 防止虚假唤醒 (Spurious Wakeup)。
-
在底层操作系统层面,线程有时会在没有收到 notify 信号的情况下莫名其妙地醒来。
-
如果使用
if,线程醒来后会直接往下执行,而不会再次检查条件是否满足,这可能导致程序逻辑错误(例如在队列为空时强行消费)。 -
使用
while,线程醒来后会再次检查条件,如果条件不满足,它会再次调用wait()继续等待。
B. IllegalMonitorStateException
如果你在没有 synchronized 的代码块中调用 wait() 或 notify(),程序会抛出 IllegalMonitorStateException 运行时异常。这是因为 Java 强制要求线程通信必须建立在持有锁的基础上。
C. 现代替代方案:Lock 和 Condition
虽然 wait/notify 是基础,但在 Java 5 引入 java.util.concurrent 包后,通常推荐使用更高级的 ReentrantLock 和 Condition。
-
synchronized$\rightarrow$ReentrantLock -
wait()$\rightarrow$Condition.await() -
notify()$\rightarrow$Condition.signal()
优势: Condition 允许通过创建多个 Condition 对象(例如 notFull 和 notEmpty),实现精准唤醒某类线程,而不是像 notifyAll 那样粗暴地唤醒所有线程。
下一步建议
您是否希望我把上面的 wait/notify 代码改写成使用 ReentrantLock 和 Condition 的版本,以便您对比新旧两种写法的区别?

这就用最通俗的大白话(配合一个生动的场景)来解释这三个东西。
为了让你秒懂,我们把“对象锁”想象成只有一个坑位的公共厕所。
把“线程”想象成想上厕所的人。
1. 它们是干嘛的?(说人话版)
-
wait()—— “占着茅坑不拉屎?不行,我出去等。”-
人话:你抢到了厕所钥匙(锁),进去了,结果发现便秘(条件不满足,比如没纸了)。你不能一直占着坑位啊,于是你把钥匙交出来(释放锁),乖乖走到厕所门外的长椅上坐着等(进入等待状态),直到有人喊你。
-
-
notify()—— “嘿,那个谁,有纸了,快醒醒!”-
人话:保洁阿姨(另一个线程)进厕所补好纸了。她冲着门外的长椅喊了一嗓子:“刚才谁在等的?有纸了!”
-
重点:她这一嗓子,只能随机叫醒一个人。这人醒了之后,还得去抢那把钥匙,抢到了才能进。
-
-
notifyAll()—— “所有睡觉的,全起来!开抢!”-
人话:保洁阿姨这次不想点名了,直接拿大喇叭喊:“都有纸了啊!”
-
重点:长椅上所有在睡觉等待的人全都被叫醒。大家一起冲向门口抢那把钥匙,谁抢到谁进,抢不到的继续等。
-
2. 存在的意义?(为什么要发明这玩意?)
你可能会问:“既然没纸(条件不满足),我在厕所里干等着不行吗?为什么要出去?”
这就是它们存在的两大核心意义:
意义一:避免“占着茅坑不干活” (资源利用)
如果在这个单人厕所里,你没纸了还不出去,一直蹲在里面干等,别人(其他线程)也进不来。
-
没有
wait():整个厕所(系统资源)就被你一个人死锁住了,程序就卡死了。 -
有了
wait():你暂时让出资源,让能干活的人(比如送纸的人)先进来操作一下。
意义二:避免“一直敲门问好了没” (CPU 效率)
如果没有这套机制,想要知道“有没有纸”,你就得每隔一秒钟去拉一下门:“有纸了吗?有纸了吗?”。
-
这叫“忙轮询”(Busy Waiting):这非常消耗精力(CPU 狂转),而且大部分时间都是在做无用功。
-
有了
notify():你就可以放心地“睡过去”,完全不消耗精力。等条件满足了,自然会有人来叫醒你。这就是不用你自己操心的“通知机制”。
3. 总结一下
| 方法 | 简单理解 | 关键动作 |
| wait() | 我先让位 | 交出锁,去睡觉,等通知。 |
| notify() | 叫醒一个 | 随机拍醒一个睡觉的人,让他准备干活。 |
| notifyAll() | 叫醒所有 | 把所有睡觉的人都拍醒,让他们竞争上岗。 |
一句话总结:
这三个方法就是为了让多个人(线程)在抢同一个资源时,能够有商有量地配合,而不是在那儿死磕或者傻等。
下一步建议
既然你已经理解了概念,要不要看看面试中最爱问的坑:
“为什么 wait 必须写在 while 循环里,而不能写在 if 里?” (这关乎到底是谁把你叫醒的,以及叫醒后是不是真的有纸了)。
这个问题的核心在于:你被叫醒了,不代表事情就一定办成了。
继续用我们刚才的“厕所(锁)+ 纸巾(条件)”的例子来解释。
1. 如果用 if (一次性判断) —— 容易出大锅
想象代码是这样的:
Java
if (没有纸) {
wait(); // 没纸,我去睡觉
}
// 醒来后直接往下冲!
擦屁股();
场景模拟:
-
你(线程A) 进厕所,发现没纸,于是执行
wait(),交出钥匙,去长椅上睡觉。 -
保洁阿姨(线程B) 来了,补了一卷纸,然后喊了一声
notify()(或者notifyAll())。 -
此时,另一个路人(线程C) 手快,抢在刚被叫醒的你之前,拿到了钥匙冲进了厕所。把阿姨刚补的那卷纸用完了! 然后走了。
-
这时候你(线程A) 终于拿到了钥匙,进了厕所。
-
灾难发生:因为你用的是
if,你醒来后不会再检查一遍有没有纸,而是直接往下执行“擦屁股”的操作。 -
结果:空手擦屁股(程序报错,数据异常,数组越界等)。
这就是所谓的 “虚假唤醒” 带来的后果:醒了不代表条件满足了。
2. 如果用 while (循环判断) —— 安全可靠
代码改成这样:
Java
while (没有纸) {
wait(); // 没纸,我去睡觉
}
// 循环退出了,说明肯定有纸了
擦屁股();
场景模拟:
-
你(线程A) 进厕所,没纸,
wait()睡觉。 -
保洁阿姨(线程B) 补纸,喊人。
-
路人(线程C) 还是手快,抢先进去把纸用完了。
-
你(线程A) 终于拿到了钥匙,进了厕所。
-
关键点来了:因为是
while,你醒来的第一件事不是“擦屁股”,而是回头再看一眼纸筒(再次执行循环判断)。 -
结果:你发现:“卧槽?怎么又没纸了?”(虽然我被叫醒了,但条件依然不满足)。
-
行动:叹口气,再次执行
wait(),把钥匙交出去,继续回长椅睡觉,等待下一次机会。
3. 总结:为什么要用 while?
哪怕是 notify 把你叫醒的,在你抢到锁的那一瞬间,世界可能已经变了。
中间可能有别的线程插队把资源抢走了,或者操作系统出于某种原因莫名其妙把你唤醒了(这叫虚假唤醒,Spurious Wakeup)。
所以,醒来后的第一件事,必须是重新检查条件。
-
if= 醒了就干,不管还在不在。 (鲁莽) -
while= 醒了再确认一眼,不在继续睡。 (稳健)
下一步建议
你现在已经搞懂了 wait/notify 的核心机制和坑。
如果想进阶一点,你想不想知道:既然 notify 这么麻烦还要防止虚假唤醒,Java 后面出的 ReentrantLock 和 Condition 是怎么优化这个流程的?(它们能实现“精准叫醒某个人”,比如只叫醒“修厕所的”或者只叫醒“上厕所的 ——见下篇博客。
更多推荐



所有评论(0)