前言
在 Java 多线程编程中,线程间的协作与通信是一个核心话题。wait()、notify() 和 notifyAll() 是 Java 提供的用于线程间通信的最基础的机制,它们定义在 Object 类中,因此每个对象都天然具备了这些方法。
一、基础概念与核心原理
1.1 三个方法的基本作用
| wait() | Object | 使当前线程进入等待状态,释放持有的对象锁 |
| wait(long timeout) | Object | 带超时时间的等待,超时后自动唤醒 |
| wait(long timeout, int nanos) | Object | 更精确的超时控制 |
| notify() | Object | 唤醒一个正在等待该对象锁的线程(随机选择) |
| notifyAll() | Object | 唤醒所有正在等待该对象锁的线程 |
1.2 核心机制:对象监视器(Monitor)
每个 Java 对象在 JVM 内部都有一个监视器(Monitor),它与对象锁紧密关联。这个监视器维护着两个关键的队列:
┌─────────────────────────────────────────┐
│ Object (锁对象) │
│ ┌─────────────────────────────────────┐│
│ │ Entry Set (入口集) ││
│ │ (等待获取锁的线程) ││
│ └─────────────────────────────────────┘│
│ ┌─────────────────────────────────────┐│
│ │ Wait Set (等待集) ││
│ │ (调用了 wait 的线程) ││
│ └─────────────────────────────────────┘│
│ │
│ Owner (当前持有锁的线程) │
└─────────────────────────────────────────┘
-
Entry Set(锁池):已经准备好竞争锁的线程集合。当线程调用 synchronized 但未能获取锁时,进入这里。
-
Wait Set(等待池):调用了 wait() 的线程被放入这里,它们暂时不参与锁竞争。
1.3 线程状态转换图
┌─────────────────────────────────────────────────────┐
│ │
▼ │
┌─────────┐ 获取锁成功 ┌─────────┐ 调用 wait() ┌─────────┐
│ NEW │ ───────────▶ │ RUNNABLE│ ─────────────▶ │ WAITING │
└─────────┘ └────┬────┘ └────┬────┘
│ │
│ 执行完毕 │ notify/notifyAll
▼ ▼
┌─────────┐ ┌─────────┐
│TERMINATED│ │ BLOCKED │
└─────────┘ │ (重新竞争锁)│
└────┬────┘
│
获取锁成功
│
▼
┌─────────┐
│RUNNABLE │
└─────────┘
二、严格的使用规则
2.1 必须在同步块/方法中调用
这是最重要的一条规则! 必须在 synchronized 代码块或方法中调用 wait()、notify()、notifyAll(),否则抛出 IllegalMonitorStateException。
// ❌ 错误示例
public void wrongWait() {
try {
this.wait(); // IllegalMonitorStateException!
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// ✅ 正确示例
public void correctWait() {
synchronized (this) {
try {
this.wait(); // OK
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2 必须由锁对象调用
调用 wait()、notify() 的对象必须和 synchronized 使用的锁是同一个对象:
private final Object lock = new Object();
// ✅ 正确
synchronized (lock) {
lock.wait(); // lock 是锁对象
}
// ❌ 错误
synchronized (lock) {
this.wait(); // this 不是锁对象!抛出异常
}
2.3 必须在 while 循环中调用 wait
官方推荐模式:
synchronized (lock) {
while (!condition) { // 必须用 while,不能用 if
lock.wait();
}
// 条件满足,执行业务逻辑
}
原因:虚假唤醒(Spurious Wakeup)
wait() 可能在没有收到 notify()/notifyAll() 的情况下被唤醒,这被称为虚假唤醒。Java 规范允许这种情况发生(为了提高底层实现的性能)。
// ❌ 危险:使用 if
synchronized (lock) {
if (!condition) {
lock.wait(); // 被虚假唤醒后,不会重新检查条件
}
// 可能 condition 仍然为 false!
}
// ✅ 安全:使用 while
synchronized (lock) {
while (!condition) { // 每次唤醒都重新检查
lock.wait();
}
}
三、为什么会丢失唤醒?
3.1 丢失唤醒问题演示
假设没有同步块保护,会发生什么:
// 错误代码:没有同步
class BuggyQueue {
private boolean ready = false;
public void waitForReady() {
if (!ready) {
try {
this.wait(); // 危险!
} catch (InterruptedException e) {}
}
}
public void setReady() {
ready = true;
this.notify();
}
}
时间线分析:
| T1 | 检查 ready,发现为 false | – | false |
| T2 | 准备执行 wait()(但还没执行) | 执行 setReady() | false |
| T3 | – | 设置 ready = true | true |
| T4 | – | 调用 notify(),但此时没有线程在等待 | true |
| T5 | 执行 wait(),进入等待 | – | true |
结果:线程 A 永远等待,因为唤醒信号已经丢失。这就是丢失唤醒(Lost Wake-Up)问题。
3.2 同步块如何解决
class SafeQueue {
private boolean ready = false;
private final Object lock = new Object();
public void waitForReady() {
synchronized (lock) { // 获取锁
while (!ready) {
lock.wait(); // 释放锁并等待
}
}
}
public void setReady() {
synchronized (lock) { // 获取同一把锁
ready = true;
lock.notify(); // 唤醒
}
}
}
修复后的时间线:
| T1 | 获取 lock 锁成功 | 等待 lock 锁 | 线程 A |
| T2 | 检查 ready=false,准备 wait | 被阻塞 | 线程 A |
| T3 | 调用 wait(),释放锁并进入等待集 | 被阻塞 | 无 |
| T4 | 已进入等待集 | 获取锁成功 | 线程 B |
| T5 | – | 设置 ready=true,调用 notify() | 线程 B |
| T6 | 被移出等待集 | 释放锁 | 无 |
| T7 | 重新竞争锁 → 成功 | – | 线程 A |
| T8 | while 重新检查 ready=true,退出循环 | – | 线程 A |
关键:线程 B 无法在线程 A 的检查和 wait 之间插⼊执行,因为线程 A 在此期间持有锁。
四、notify vs notifyAll:如何选择?
4.1 行为差异
// 多个线程等待同一个条件
synchronized (lock) {
while (resource == null) {
lock.wait(); // 线程1、线程2、线程3都在这里等待
}
}
// 生产者
synchronized (lock) {
resource = new Resource();
lock.notify(); // 只唤醒一个等待线程
// lock.notifyAll(); // 唤醒所有等待线程
}
| 唤醒数量 | 1 个(JVM 选择,不可控) | 所有等待的线程 |
| 效率 | 较高(减少上下文切换) | 较低(可能唤醒不必要的线程) |
| 安全性 | 较低(可能唤醒错误类型的线程) | 较高(所有线程都有机会检查条件) |
4.2 选择原则
使用 notify() 的场景(较少见):
-
所有等待线程的条件相同
-
每次只有一个线程能够成功执行后续操作
-
不存在线程饥饿的风险
// 适合 notify() 的示例:有界缓冲区,单消费者
class SimpleBuffer {
private Object item;
private boolean empty = true;
public synchronized Object take() throws InterruptedException {
while (empty) {
wait();
}
empty = true;
notify(); // 唤醒生产者即可
return item;
}
public synchronized void put(Object obj) throws InterruptedException {
while (!empty) {
wait();
}
item = obj;
empty = false;
notify(); // 唤醒消费者即可
}
}
使用 notifyAll() 的场景(更常用、更安全):
-
多个等待线程条件不同
-
不确定哪种类型的线程应该被唤醒
-
任何时候都更安全(宁可多唤醒,不能漏唤醒)
// 必须使用 notifyAll() 的示例:多条件等待
class Account {
private int balance = 0;
public synchronized void withdraw(int amount) throws InterruptedException {
while (balance < amount) {
wait(); // 等待存款
}
balance -= amount;
}
public synchronized void deposit(int amount) throws InterruptedException {
balance += amount;
notifyAll(); // 必须唤醒所有!因为可能有多个取款线程在等待
}
}
4.3 一个经典陷阱
// ❌ 错误:条件不匹配时使用 notify
class WrongNotify {
private boolean conditionA = false;
private boolean conditionB = false;
public synchronized void waitA() throws InterruptedException {
while (!conditionA) wait();
}
public synchronized void waitB() throws InterruptedException {
while (!conditionB) wait();
}
public synchronized void setA() {
conditionA = true;
notify(); // 如果唤醒的是 waitB(),它会继续等待,永远无法被唤醒
}
}
问题:setA() 中调用 notify() 可能唤醒正在 waitB() 上的线程,而这个线程检查 conditionB 仍然为 false,会继续等待。而 conditionA 已经为 true,但 waitA() 的线程却永远得不到通知。
解决方案:使用 notifyAll() 或者使用不同的锁对象。
五、完整实战案例:生产者-消费者模型
5.1 有界缓冲区实现
import java.util.LinkedList;
import java.util.Queue;
/**
* 使用 wait/notifyAll 实现的线程安全有界缓冲区
*/
public class BoundedBuffer<T> {
private final Queue<T> buffer = new LinkedList<>();
private final int capacity;
public BoundedBuffer(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("Capacity must be positive");
}
this.capacity = capacity;
}
/**
* 生产者:向缓冲区添加元素
* 如果缓冲区已满,则等待直到有空间
*/
public synchronized void put(T item) throws InterruptedException {
// 使用 while 循环,防止虚假唤醒
while (buffer.size() == capacity) {
System.out.println(Thread.currentThread().getName() +
" 缓冲区已满,等待消费…");
wait(); // 等待消费者取走元素
}
buffer.offer(item);
System.out.println(Thread.currentThread().getName() +
" 生产: " + item + ", 当前大小: " + buffer.size());
// 通知所有等待的消费者(也可能有生产者等待,但 notifyAll 更安全)
notifyAll();
}
/**
* 消费者:从缓冲区取出元素
* 如果缓冲区为空,则等待直到有元素
*/
public synchronized T take() throws InterruptedException {
while (buffer.isEmpty()) {
System.out.println(Thread.currentThread().getName() +
" 缓冲区为空,等待生产…");
wait(); // 等待生产者添加元素
}
T item = buffer.poll();
System.out.println(Thread.currentThread().getName() +
" 消费: " + item + ", 当前大小: " + buffer.size());
// 通知所有等待的生产者
notifyAll();
return item;
}
public synchronized int size() {
return buffer.size();
}
// ———- 测试代码 ———-
public static void main(String[] args) {
BoundedBuffer<Integer> buffer = new BoundedBuffer<>(5);
// 创建 3 个生产者线程
for (int i = 1; i <= 3; i++) {
final int producerId = i;
new Thread(() -> {
try {
for (int j = 1; j <= 10; j++) {
buffer.put(producerId * 100 + j);
Thread.sleep(100); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer-" + i).start();
}
// 创建 2 个消费者线程
for (int i = 1; i <= 2; i++) {
final int consumerId = i;
new Thread(() -> {
try {
for (int j = 1; j <= 15; j++) {
Integer item = buffer.take();
Thread.sleep(150); // 模拟消费耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer-" + i).start();
}
}
}
5.2 运行输出示例
Producer-1 生产: 101, 当前大小: 1
Producer-2 生产: 201, 当前大小: 2
Consumer-1 消费: 101, 当前大小: 1
Producer-3 生产: 301, 当前大小: 2
Consumer-2 消费: 201, 当前大小: 1
…
Producer-1 缓冲区已满,等待消费…
Consumer-1 消费: 302, 当前大小: 4
Producer-1 生产: 104, 当前大小: 5
5.3 优化版本:使用条件分离
当有多个不同条件时,使用不同的锁对象可以提高效率:
/**
* 优化版本:使用不同的条件锁,避免不必要的唤醒
*/
public class OptimizedBuffer<T> {
private final Queue<T> buffer = new LinkedList<>();
private final int capacity;
private final Object notFull = new Object(); // 缓冲区未满条件
private final Object notEmpty = new Object(); // 缓冲区非空条件
public OptimizedBuffer(int capacity) {
this.capacity = capacity;
}
public void put(T item) throws InterruptedException {
synchronized (notFull) {
while (buffer.size() == capacity) {
notFull.wait(); // 只等待"未满"条件
}
}
synchronized (notEmpty) {
buffer.offer(item);
notEmpty.notifyAll(); // 只通知等待"非空"的消费者
}
}
public T take() throws InterruptedException {
synchronized (notEmpty) {
while (buffer.isEmpty()) {
notEmpty.wait(); // 只等待"非空"条件
}
}
synchronized (notFull) {
T item = buffer.poll();
notFull.notifyAll(); // 只通知等待"未满"的生产者
return item;
}
}
}
六、常见陷阱与最佳实践
6.1 陷阱清单
| 不在同步块中调用 | IllegalMonitorStateException | 始终在 synchronized 中调用 |
| 使用 if 而非 while | 虚假唤醒导致条件不满足时继续执行 | 必须使用 while 循环 |
| 使用 notify() 但有多条件 | 线程永久阻塞 | 使用 notifyAll() 或分离锁 |
| 在循环外修改条件变量 | 可见性问题 | 在同步块内修改和读取 |
| 忘记在 catch 中恢复中断状态 | 中断状态丢失 | 使用 Thread.currentThread().interrupt() |
| 在持有锁时执行耗时操作 | 其他线程长时间阻塞 | 缩小同步块范围 |
6.2 最佳实践总结
/**
* 标准模板 – 请牢记!
*/
public class BestPractice {
private final Object lock = new Object();
private boolean condition = false;
// 等待方模板
public void waitForCondition() throws InterruptedException {
synchronized (lock) {
while (!condition) { // 1. while 循环
lock.wait(); // 2. 释放锁并等待
}
// 3. 条件满足,执行业务
}
}
// 通知方模板
public void changeCondition() {
synchronized (lock) {
condition = true; // 1. 改变条件
lock.notifyAll(); // 2. 通知所有等待线程
}
}
}
6.3 处理 InterruptedException 的正确方式
// ❌ 错误:吞掉中断
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
// 什么都不做 – 中断信息丢失!
}
}
// ✅ 正确方式 1:重新抛出
public void method() throws InterruptedException {
synchronized (lock) {
lock.wait(); // 让上层处理
}
}
// ✅ 正确方式 2:恢复中断状态
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断标志
// 根据业务决定是否退出
}
}
七、与其他并发工具的比较
7.1 wait/notify vs Condition
| 所属类 | Object | ReentrantLock |
| 多条件等待 | 需要多个锁对象 | 一个锁可创建多个 Condition |
| 公平性 | 不支持 | 可配置 |
| 精确唤醒 | 只有 notify/notifyAll | signal/signalAll,可精确到 Condition |
| API 易用性 | 较原始 | 更丰富(await/signal) |
| 使用场景 | 简单场景 | 复杂条件等待场景 |
// Condition 示例:更优雅的多条件等待
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
lock.lock();
try {
while (isFull) {
notFull.await();
}
// 生产
notEmpty.signal();
} finally {
lock.unlock();
}
7.2 wait/notify vs BlockingQueue
| 复杂度 | 需要手动管理同步和条件 | 封装好,开箱即用 |
| 灵活性 | 很高,可定制任意条件 | 有限,预定义队列行为 |
| 代码量 | 较多 | 很少 |
| 适用场景 | 需要自定义复杂同步逻辑 | 标准生产者-消费者 |
// BlockingQueue 简化版生产者-消费者
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产者
queue.put(item); // 自动阻塞直到有空间
// 消费者
Integer item = queue.take(); // 自动阻塞直到有元素
wait() 和 sleep() 的区别?
| 所属类 | Object | Thread |
| 是否释放锁 | ✅ 释放 | ❌ 不释放 |
| 是否需要同步块 | ✅ 必须 | ❌ 不需要 |
| 唤醒方式 | notify/notifyAll | 时间到或 interrupt |
| 用途 | 线程间通信 | 暂停执行 |
以上均为个人观点!
以上均为个人观点!
以上均为个人观点!



