Java并发编程:线程的等待和通知机制
前言
在多线程编程中,线程间的协作与通信是至关重要的。Java 提供了 wait()、notify() 和 notifyAll() 方法来实现线程间的等待和通知机制,这些方法是实现生产者-消费者模式、线程池等高级并发组件的基础。
由于 Java 中的 Object 类是所有类的父类,这些方法被放在 Object 类中,确保每个对象都具备线程间通信的能力。
一、wait() 方法
1.1 方法签名
public final void wait() throws InterruptedException
1.2 核心行为
当一个线程调用共享变量的 wait() 方法时,该线程会被阻塞挂起,直到发生以下情况之一才会返回:
-
其他线程调用了该共享对象的
notify()或notifyAll()方法 -
其他线程调用了该线程的
interrupt()方法(此时抛出InterruptedException)
1.3 重要前提:监视器锁
调用 wait() 方法的线程必须事先获取该对象的监视器锁,否则会抛出 IllegalMonitorStateException 异常。
1.4 如何获取监视器锁?
方式一:synchronized 同步代码块
synchronized (sharedObject) {
// 此处已获取 sharedObject 的监视器锁
sharedObject.wait();
}
方式二:synchronized 实例方法
public synchronized void method() {
// 当前对象 this 的监视器锁已被获取
this.wait();
}
1.5 虚假唤醒(Spurious Wakeup)
一个线程从挂起状态变为可运行状态(被唤醒),即使没有被其他线程调用 notify()、notifyAll(),也没有被中断或超时,这种情况称为虚假唤醒。
⚠️ 重要:虽然虚假唤醒在实际应用中很少发生,但必须防范!
1.6 正确的 wait() 使用模式
synchronized (obj) {
while (条件不满足) {
obj.wait();
}
// 执行条件满足后的操作
}
为什么使用 while 循环?
-
防止虚假唤醒
-
当线程被唤醒后,重新检查条件是否满足
-
条件不满足则继续等待,直到真正满足条件
二、生产者和消费者示例
下面通过一个完整的示例来加深理解:
public class ProducerConsumerDemo {
private static final Queue<String> queue = new LinkedList<>();
private static final int MAX_SIZE = 1;
/**
* 生产者线程
*/
static class Producer extends Thread {
@Override
public void run() {
synchronized (queue) {
// 队列已满,等待消费者消费
while (queue.size() == MAX_SIZE) {
try {
System.out.println("队列已满,生产者等待...");
// 释放锁并等待
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产元素
String element = "元素-" + System.currentTimeMillis();
queue.add(element);
System.out.println("生产: " + element);
// 通知消费者
queue.notifyAll();
}
}
}
/**
* 消费者线程
*/
static class Consumer extends Thread {
@Override
public void run() {
synchronized (queue) {
// 队列为空,等待生产者生产
while (queue.isEmpty()) {
try {
System.out.println("队列为空,消费者等待...");
// 释放锁并等待
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费元素
String element = queue.poll();
System.out.println("消费: " + element);
// 通知生产者
queue.notifyAll();
}
}
}
public static void main(String[] args) {
Producer producer = new Producer();
Consumer consumer = new Consumer();
producer.start();
consumer.start();
}
}
为什么 wait() 要释放锁?
如果线程 A 调用 wait() 后不释放锁,那么:
-
其他生产者和所有消费者线程都被阻塞
-
线程 A 也被挂起
-
整个系统陷入死锁状态
释放锁是为了打破死锁必要条件中的持有并等待原则。
三、wait(long timeout) 方法
3.1 方法签名
public final native void wait(long timeout) throws InterruptedException
3.2 与 wait() 的区别
| 特性 | wait() | wait(timeout) |
|---|---|---|
| 超时机制 | 无,需要被唤醒 | 超时后自动返回 |
| 超时参数 | 不支持 | 支持(毫秒) |
| timeout=0 | - | 等同于 wait() |
| 负值参数 | - | 抛出 IllegalArgumentException |
3.3 使用示例
synchronized (lock) {
while (condition) {
// 最多等待 5000 毫秒
lock.wait(5000);
// 超时返回后,需要重新检查条件
}
}
四、notify() 方法
4.1 方法签名
public final native void notify()
4.2 核心特点
-
随机唤醒:唤醒一个在该对象上等待的线程(随机)
-
不立即执行:被唤醒的线程需要重新获取监视器锁后才能继续执行
-
需要锁:调用
notify()的线程也必须持有该对象的监视器锁
4.3 执行流程示意图
线程A获得锁 → 调用wait() → 释放锁并进入等待队列
↓
线程B获得锁 → 调用notify() → 唤醒线程A(从等待队列移到锁竞争队列)
↓
线程B释放锁 → 线程A竞争锁 → 获取锁后从wait()返回
五、notifyAll() 方法
5.1 方法签名
public final native void notifyAll()
5.2 与 notify() 的对比
| 方法 | 唤醒数量 | 适用场景 |
|---|---|---|
| notify() | 1个线程 | 多个线程等待同一条件,且条件变化后只需唤醒一个 |
| notifyAll() | 所有等待线程 | 多个线程等待不同条件,或无法确定唤醒哪个 |
5.3 完整对比示例
public class NotifyVsNotifyAllDemo {
private static final Object resource = new Object();
static class WaitingThread extends Thread {
private final String name;
WaitingThread(String name) {
this.name = name;
}
@Override
public void run() {
synchronized (resource) {
try {
System.out.println(name + " 获取锁,开始等待...");
resource.wait();
System.out.println(name + " 被唤醒,继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
// 启动两个等待线程
new WaitingThread("线程A").start();
new WaitingThread("线程B").start();
Thread.sleep(1000);
// 使用 notify() - 只唤醒一个
synchronized (resource) {
System.out.println("\n使用 notify(),只唤醒一个线程");
resource.notify();
}
Thread.sleep(2000);
// 再次启动两个等待线程
new WaitingThread("线程C").start();
new WaitingThread("线程D").start();
Thread.sleep(1000);
// 使用 notifyAll() - 唤醒全部
synchronized (resource) {
System.out.println("\n使用 notifyAll(),唤醒所有线程");
resource.notifyAll();
}
}
}
输出结果:
线程A 获取锁,开始等待... 线程B 获取锁,开始等待... 使用 notify(),只唤醒一个线程 线程A 被唤醒,继续执行 使用 notifyAll(),唤醒所有线程 线程C 被唤醒,继续执行 线程D 被唤醒,继续执行
六、中断响应
当一个线程因调用 wait() 方法被阻塞时,如果其他线程中断了该线程,会抛出 InterruptedException:
public class WaitInterruptDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitingThread = new Thread(() -> {
try {
synchronized (lock) {
System.out.println("线程开始等待...");
lock.wait();
System.out.println("这行不会被执行");
}
} catch (InterruptedException e) {
System.out.println("线程被中断,捕获 InterruptedException");
e.printStackTrace();
}
});
waitingThread.start();
// 等待1秒确保线程进入等待状态
Thread.sleep(1000);
System.out.println("主线程中断等待线程");
waitingThread.interrupt();
}
}
输出结果:
线程开始等待...
主线程中断等待线程
线程被中断,捕获 InterruptedException
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
...
七、常见陷阱与最佳实践
7.1 常见异常
| 异常 | 原因 | 解决方案 |
|---|---|---|
| IllegalMonitorStateException | 未在同步块内调用 wait/notify | 确保在 synchronized 内部调用 |
| IllegalArgumentException | wait(timeout) 参数为负数 | 检查超时参数 |
7.2 最佳实践总结
// ✅ 正确写法
synchronized (lock) {
while (!condition) {
lock.wait();
}
// 执行操作
}
// ❌ 错误写法1:没有获取锁
lock.wait(); // 抛出 IllegalMonitorStateException
// ❌ 错误写法2:使用 if 而不是 while
synchronized (lock) {
if (!condition) { // 无法防止虚假唤醒
lock.wait();
}
}
7.3 核心要点
-
始终在同步块内调用 wait/notify
-
始终使用 while 循环条件判断
-
优先使用 notifyAll() 而非 notify()
-
处理 InterruptedException 异常
-
wait() 会释放锁,notify() 不会释放锁
八、总结
| 方法 | 作用 | 释放锁 | 需要锁 |
|---|---|---|---|
| wait() | 阻塞当前线程 | ✅ 是 | ✅ 是 |
| wait(timeout) | 限时阻塞 | ✅ 是 | ✅ 是 |
| notify() | 唤醒一个等待线程 | ❌ 否 | ✅ 是 |
| notifyAll() | 唤醒所有等待线程 | ❌ 否 | ✅ 是 |
核心记忆口诀:
同步块内调等待,while 循环防虚假。
通知唤醒不释锁,等待释锁是关键。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)