wait和notify机制详解
wait和notify机制详解
为什么需要wait/notify
问题场景:主动等待 vs 被动通知
####场景:厨师与服务员
// ❌ 不好的实现:忙等待(Busy Waiting)
public class BadWaiting {private static boolean foodReady = false;public static void main(String[] args) {// 服务员线程Thread waiter = new Thread(() -> {System.out.println("服务员:等待食物...");// 不停地检查,浪费CPUwhile (!foodReady) {// 忙等待,CPU空转}System.out.println("服务员:食物好了,去送餐");}, "服务员");// 厨师线程Thread chef = new Thread(() -> {System.out.println("厨师:开始做饭");try {Thread.sleep(3000); // 模拟做饭} catch (InterruptedException e) {e.printStackTrace();}System.out.println("厨师:食物做好了");foodReady = true;}, "厨师");waiter.start();chef.start();}
}
问题:
- ❌ 服务员线程一直循环检查,浪费CPU
- ❌ 即使厨师还没开始做,服务员也在空转
- ❌ 效率低下
// ✅ 好的实现:wait/notify
public class GoodWaiting {private static boolean foodReady = false;private static final Object lock = new Object();public static void main(String[] args) {// 服务员线程Thread waiter = new Thread(() -> {synchronized (lock) {System.out.println("服务员:等待食物...");try {while (!foodReady) {lock.wait(); // 释放锁,进入等待,不消耗CPU}} catch (InterruptedException e) {e.printStackTrace();}System.out.println("服务员:食物好了,去送餐");}}, "服务员");// 厨师线程Thread chef = new Thread(() -> {System.out.println("厨师:开始做饭");try {Thread.sleep(3000); // 模拟做饭} catch (InterruptedException e) {e.printStackTrace();}synchronized (lock) {System.out.println("厨师:食物做好了");foodReady = true;lock.notify(); // 通知服务员}}, "厨师");waiter.start();chef.start();}
}
优点:
- ✅ 服务员等待时不消耗CPU
- ✅ 厨师做好后主动通知,响应及时
- ✅ 效率高
wait/notify基本用法
核心方法
Object类提供的三个方法:
public class Object {// 等待,直到被notify/notifyAll或中断public final void wait() throws InterruptedException// 等待指定时间(毫秒)public final void wait(long timeout) throws InterruptedException// 等待指定时间(毫秒+纳秒)public final void wait(long timeout, int nanos) throws InterruptedException// 唤醒一个等待的线程public final native void notify()// 唤醒所有等待的线程public final native void notifyAll()
}
基本使用规则
规则1:必须在synchronized块中调用
// ❌ 错误:不在synchronized块中
public class WrongUsage {private Object lock = new Object();public void method() {try {lock.wait(); // IllegalMonitorStateException!} catch (InterruptedException e) {e.printStackTrace();}}
}// ✅ 正确:在synchronized块中
public class CorrectUsage {private Object lock = new Object();public void method() {synchronized (lock) {try {lock.wait(); // 正确} catch (InterruptedException e) {e.printStackTrace();}}}
}
原因:wait/notify操作的是Monitor,必须先获得Monitor的ownership。
规则2:使用while而不是if检查条件
// ❌ 错误:使用if
synchronized (lock) {if (!condition) {lock.wait(); // 虚假唤醒时会直接继续执行}// 执行操作
}// ✅ 正确:使用while
synchronized (lock) {while (!condition) {lock.wait(); // 虚假唤醒后会重新检查条件}// 执行操作
}
原因:防止虚假唤醒(Spurious Wakeup)。
wait()方法详解
wait()的作用
当前线程调用lock.wait()时:步骤1:释放锁Owner = null步骤2:当前线程进入WaitSet状态变为WAITING步骤3:不再消耗CPU线程阻塞等待步骤4:被唤醒后从WaitSet移到EntryList重新竞争锁步骤5:获得锁后从wait()返回继续执行
wait(long timeout)
synchronized (lock) {try {lock.wait(5000); // 最多等待5秒} catch (InterruptedException e) {e.printStackTrace();}// 被唤醒或超时后继续执行
}
两种退出方式:
- 被notify/notifyAll唤醒
- 超时自动唤醒
notify()方法详解
notify() vs notifyAll()
// notify():只唤醒一个等待的线程
synchronized (lock) {lock.notify(); // 唤醒WaitSet中的一个线程(随机)
}// notifyAll():唤醒所有等待的线程
synchronized (lock) {lock.notifyAll(); // 唤醒WaitSet中的所有线程
}
选择建议:
- 如果只需要唤醒一个线程:
notify() - 如果不确定唤醒哪个:
notifyAll()(更安全)
notify()不会立即释放锁
重要:调用notify()后,当前线程继续持有锁!
public class NotifyNoRelease {private static final Object lock = new Object();public static void main(String[] args) throws InterruptedException {// 等待线程Thread t1 = new Thread(() -> {synchronized (lock) {System.out.println("t1: 开始等待");try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1: 被唤醒,继续执行");}}, "t1");// 通知线程Thread t2 = new Thread(() -> {synchronized (lock) {System.out.println("t2: 准备唤醒t1");lock.notify();System.out.println("t2: 已调用notify,但还持有锁");try {Thread.sleep(2000); // 继续持有锁2秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2: 即将释放锁");}System.out.println("t2: 已释放锁");}, "t2");t1.start();Thread.sleep(100); // 确保t1先waitt2.start();}
}
输出:
t1: 开始等待
t2: 准备唤醒t1
t2: 已调用notify,但还持有锁
(等待2秒)
t2: 即将释放锁
t2: 已释放锁
t1: 被唤醒,继续执行 ← t2释放锁后,t1才能继续
wait/notify原理
wait()方法完整流程图
notify()方法完整流程图
notifyAll()方法流程图
Monitor状态转换完整时序图
┌───────────────────────────────────────────────────────────────────┐
│ wait/notify 的 Monitor 状态完整转换过程 │
└───────────────────────────────────────────────────────────────────┘时刻T1: Thread-1获取锁
┌──────────────┐
│ Owner │
│ Thread-1 ✅ │
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘时刻T2: Thread-1调用wait()
┌──────────────┐
│ Owner │
│ null ⚠️ │ ← 锁被释放
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ Thread-1 😴 │ ← 进入等待集合,WAITING状态
└──────────────┘时刻T3: Thread-2竞争锁成功
┌──────────────┐
│ Owner │
│ Thread-2 ✅ │
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ Thread-1 😴 │
└──────────────┘时刻T4: Thread-2调用notify()
┌──────────────┐
│ Owner │
│ Thread-2 ✅ │ ← Thread-2继续持有锁!
├──────────────┤
│ EntryList │
│ Thread-1 🚶 │ ← Thread-1被移到这里,BLOCKED状态
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘时刻T5: Thread-2释放锁
┌──────────────┐
│ Owner │
│ null ⚠️ │ ← 锁被释放
├──────────────┤
│ EntryList │
│ Thread-1 💪 │ ← Thread-1尝试竞争锁
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘时刻T6: Thread-1重新获得锁
┌──────────────┐
│ Owner │
│ Thread-1 ✅ │ ← Thread-1重新获得锁
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘
Thread-1从wait()返回,继续执行 ✅
Monitor视角
初始状态:
┌──────────────┐
│ Owner │
│ null │
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘Thread-1获取锁:
┌──────────────┐
│ Owner │
│ Thread-1 ←── │ 执行中
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘Thread-1调用wait():
┌──────────────┐
│ Owner │
│ null ←── │ 锁被释放
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ Thread-1 ←── │ 进入等待集合
└──────────────┘Thread-2获取锁并notify():
┌──────────────┐
│ Owner │
│ Thread-2 │ 继续执行
├──────────────┤
│ EntryList │
│ Thread-1 ←── │ 被移到这里
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘Thread-2释放锁,Thread-1获得锁:
┌──────────────┐
│ Owner │
│ Thread-1 ←── │ 重新获得锁
├──────────────┤
│ EntryList │
│ (空) │
├──────────────┤
│ WaitSet │
│ (空) │
└──────────────┘
虚假唤醒(Spurious Wakeup)
什么是虚假唤醒:线程在没有被notify/notifyAll的情况下,从wait()返回。
原因:
- 操作系统层面的实现细节
- 线程可能被信号中断
- JVM实现的优化
解决方法:使用while循环检查条件
// ❌ 使用if,虚假唤醒会导致问题
synchronized (lock) {if (!condition) {lock.wait();}// 虚假唤醒后,condition可能还是false,但会继续执行performAction(); // 错误!
}// ✅ 使用while,虚假唤醒后重新检查
synchronized (lock) {while (!condition) {lock.wait();}// 只有condition为true才会执行performAction(); // 正确!
}
经典应用模式
模式1:生产者-消费者
单生产者-单消费者
import java.util.LinkedList;
import java.util.Queue;public class ProducerConsumer {private static final int MAX_SIZE = 5;private Queue<Integer> queue = new LinkedList<>();private final Object lock = new Object();// 生产者public void produce() {int value = 0;while (true) {synchronized (lock) {// 队列满了,等待while (queue.size() == MAX_SIZE) {try {System.out.println("队列已满,生产者等待");lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 生产queue.add(value);System.out.println("生产: " + value + ",队列大小: " + queue.size());value++;// 通知消费者lock.notify();}// 模拟生产耗时try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}// 消费者public void consume() {while (true) {synchronized (lock) {// 队列空了,等待while (queue.isEmpty()) {try {System.out.println("队列为空,消费者等待");lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 消费int value = queue.poll();System.out.println("消费: " + value + ",队列大小: " + queue.size());// 通知生产者lock.notify();}// 模拟消费耗时try {Thread.sleep(1500);} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {ProducerConsumer pc = new ProducerConsumer();Thread producer = new Thread(() -> pc.produce(), "生产者");Thread consumer = new Thread(() -> pc.consume(), "消费者");producer.start();consumer.start();}
}
运行结果:
生产: 0,队列大小: 1
生产: 1,队列大小: 2
消费: 0,队列大小: 1
生产: 2,队列大小: 2
消费: 1,队列大小: 1
生产: 3,队列大小: 2
...
多生产者-多消费者
public class MultiProducerConsumer {private static final int MAX_SIZE = 5;private Queue<Integer> queue = new LinkedList<>();private final Object lock = new Object();private int value = 0;// 生产者public void produce() {while (true) {synchronized (lock) {while (queue.size() == MAX_SIZE) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}int item = value++;queue.add(item);System.out.println(Thread.currentThread().getName() + " 生产: " + item + ",队列: " + queue.size());lock.notifyAll(); // 多个消费者,使用notifyAll}try {Thread.sleep((int)(Math.random() * 1000));} catch (InterruptedException e) {e.printStackTrace();}}}// 消费者public void consume() {while (true) {synchronized (lock) {while (queue.isEmpty()) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}int item = queue.poll();System.out.println(Thread.currentThread().getName() + " 消费: " + item + ",队列: " + queue.size());lock.notifyAll(); // 多个生产者,使用notifyAll}try {Thread.sleep((int)(Math.random() * 1500));} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) {MultiProducerConsumer mpc = new MultiProducerConsumer();// 2个生产者for (int i = 1; i <= 2; i++) {new Thread(() -> mpc.produce(), "生产者-" + i).start();}// 3个消费者for (int i = 1; i <= 3; i++) {new Thread(() -> mpc.consume(), "消费者-" + i).start();}}
}
模式2:同步屏障(Barrier)
等待所有线程到达某个点后再继续执行:
public class SimpleBarrier {private final int parties;private int count;private final Object lock = new Object();public SimpleBarrier(int parties) {this.parties = parties;this.count = parties;}public void await() throws InterruptedException {synchronized (lock) {count--;if (count > 0) {// 还有线程没到,等待System.out.println(Thread.currentThread().getName() + " 等待,还差 " + count + " 个线程");lock.wait();} else {// 最后一个线程到达,唤醒所有等待的线程System.out.println(Thread.currentThread().getName() + " 是最后一个,唤醒所有线程");count = parties; // 重置计数lock.notifyAll();}}}public static void main(String[] args) {SimpleBarrier barrier = new SimpleBarrier(3);for (int i = 1; i <= 3; i++) {int id = i;new Thread(() -> {try {System.out.println("线程-" + id + " 准备中...");Thread.sleep(id * 1000); // 模拟准备时间不同System.out.println("线程-" + id + " 到达屏障");barrier.await(); // 等待其他线程System.out.println("线程-" + id + " 通过屏障,继续执行");} catch (InterruptedException e) {e.printStackTrace();}}, "线程-" + id).start();}}
}
输出:
线程-1 准备中...
线程-2 准备中...
线程-3 准备中...
线程-1 到达屏障
线程-1 等待,还差 2 个线程
线程-2 到达屏障
线程-2 等待,还差 1 个线程
线程-3 到达屏障
线程-3 是最后一个,唤醒所有线程
线程-3 通过屏障,继续执行
线程-1 通过屏障,继续执行
线程-2 通过屏障,继续执行
模式3:顺序执行
确保线程按特定顺序执行:
public class SequentialExecution {private int flag = 1;private final Object lock = new Object();public void printA() {for (int i = 0; i < 10; i++) {synchronized (lock) {while (flag != 1) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print("A");flag = 2;lock.notifyAll();}}}public void printB() {for (int i = 0; i < 10; i++) {synchronized (lock) {while (flag != 2) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print("B");flag = 3;lock.notifyAll();}}}public void printC() {for (int i = 0; i < 10; i++) {synchronized (lock) {while (flag != 3) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("C");flag = 1;lock.notifyAll();}}}public static void main(String[] args) {SequentialExecution se = new SequentialExecution();new Thread(() -> se.printA()).start();new Thread(() -> se.printB()).start();new Thread(() -> se.printC()).start();}
}
输出:
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
ABC
常见陷阱与最佳实践
陷阱1:不在synchronized块中调用
// ❌ 错误
lock.wait(); // IllegalMonitorStateException// ✅ 正确
synchronized (lock) {lock.wait();
}
陷阱2:使用if而不是while
// ❌ 错误:虚假唤醒会导致问题
synchronized (lock) {if (!condition) {lock.wait();}doSomething(); // 可能condition还是false
}// ✅ 正确:使用while循环
synchronized (lock) {while (!condition) {lock.wait();}doSomething(); // 确保condition为true
}
陷阱3:notify()唤醒了错误的线程
// ❌ 问题:notify()随机唤醒一个线程
synchronized (lock) {lock.notify(); // 可能唤醒了生产者而不是消费者
}// ✅ 解决方案1:使用notifyAll()
synchronized (lock) {lock.notifyAll(); // 唤醒所有线程,让它们自己判断
}// ✅ 解决方案2:使用Condition(见ReentrantLock)
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 可以精确唤醒特定条件的线程
陷阱4:忘记处理InterruptedException
// ❌ 错误:吞掉异常
synchronized (lock) {try {lock.wait();} catch (InterruptedException e) {// 什么都不做}
}// ✅ 正确:恢复中断状态或向上传播
synchronized (lock) {try {lock.wait();} catch (InterruptedException e) {Thread.currentThread().interrupt(); // 恢复中断状态// 或者重新抛出}
}
最佳实践
1. 总是使用while循环
synchronized (lock) {while (!condition) {lock.wait();}// 执行操作
}
2. 尽量使用notifyAll()
// 除非确定只需要唤醒一个线程,否则使用notifyAll()
synchronized (lock) {lock.notifyAll(); // 更安全
}
3. 条件谓词要简单明确
// ✅ 好:条件明确
while (queue.isEmpty()) {lock.wait();
}// ❌ 不好:条件复杂
while (queue.size() < MIN_SIZE && !shutdown && time < deadline) {lock.wait(); // 太复杂,难以维护
}
4. 考虑使用更高级的工具
// 如果wait/notify太复杂,考虑使用:
// - BlockingQueue(生产者-消费者)
// - CountDownLatch(等待多个线程)
// - CyclicBarrier(同步屏障)
// - Semaphore(资源控制)
// - ReentrantLock + Condition(更灵活的等待通知)// 例如:使用BlockingQueue简化生产者-消费者
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 生产
queue.put(item); // 自动等待队列有空间
// 消费
int item = queue.take(); // 自动等待队列有元素
🎯 知识点总结
wait/notify核心要点
| 方法 | 作用 | 注意事项 |
|---|---|---|
wait() | 释放锁并等待 | 必须在synchronized块中 |
notify() | 唤醒一个等待的线程 | 不立即释放锁 |
notifyAll() | 唤醒所有等待的线程 | 更安全,推荐使用 |
标准模式
synchronized (lock) {while (!condition) {lock.wait();}// 执行操作condition = false;lock.notifyAll();
}
关键点
- ✅ 必须在synchronized块中调用
- ✅ 使用while而不是if检查条件
- ✅ 优先使用notifyAll()
- ✅ 正确处理InterruptedException
- ✅ 考虑使用更高级的并发工具
💡 常见面试题
Q1:wait()和sleep()的区别?
答:
- wait()是Object的方法,sleep()是Thread的静态方法
- wait()会释放锁,sleep()不释放锁
- wait()必须在synchronized块中调用,sleep()可以在任何地方调用
- wait()需要notify()唤醒,sleep()时间到自动醒来
Q2:为什么wait/notify必须在synchronized块中?
答:因为wait/notify操作的是对象的Monitor,而只有获得了Monitor的ownership(即获得了锁)才能操作Monitor。如果不在synchronized块中调用,会抛出IllegalMonitorStateException。
Q3:为什么要用while而不是if检查条件?
答:因为存在虚假唤醒(Spurious Wakeup)。线程可能在没有被notify的情况下从wait()返回。使用while循环可以在被唤醒后重新检查条件,确保条件满足才继续执行。
Q4:notify()和notifyAll()有什么区别?
答:
- notify():随机唤醒WaitSet中的一个线程
- notifyAll():唤醒WaitSet中的所有线程
- 推荐使用notifyAll(),因为更安全,可以避免唤醒错误的线程
Q5:调用notify()后会立即释放锁吗?
答:不会。调用notify()后,当前线程继续持有锁,直到synchronized块结束才释放锁。被唤醒的线程会从WaitSet移到EntryList,等待获取锁。
