欢迎光临
我们一直在努力

深入理解 Java 中的 wait、notify 和 notifyAll

前言

        在 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();
}
}

时间线分析:

时间线程 A(消费者)线程 B(生产者)ready 状态
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(); // 唤醒
}
}
}

修复后的时间线:

时间线程 A线程 B锁持有者
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(); // 唤醒所有等待线程
}

特性notify()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

特性wait/notifyCondition (JUC)
所属类 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

特性wait/notifyBlockingQueue
复杂度 需要手动管理同步和条件 封装好,开箱即用
灵活性 很高,可定制任意条件 有限,预定义队列行为
代码量 较多 很少
适用场景 需要自定义复杂同步逻辑 标准生产者-消费者

// BlockingQueue 简化版生产者-消费者
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

// 生产者
queue.put(item); // 自动阻塞直到有空间

// 消费者
Integer item = queue.take(); // 自动阻塞直到有元素

wait() 和 sleep() 的区别?

区别wait()sleep()
所属类 Object Thread
是否释放锁 ✅ 释放 ❌ 不释放
是否需要同步块 ✅ 必须 ❌ 不需要
唤醒方式 notify/notifyAll 时间到或 interrupt
用途 线程间通信 暂停执行

以上均为个人观点!

以上均为个人观点!

以上均为个人观点!

赞(0)
未经允许不得转载:171主机测评 » 深入理解 Java 中的 wait、notify 和 notifyAll
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址