Java Object wait() 方法详解:线程同步中的“暂停与唤醒”机制
在 Java 多线程编程中,我们经常需要控制线程之间的协作。比如一个线程在等待某个资源就绪,另一个线程负责提供这个资源。这时候,Java Object wait() 方法 就派上了用场。它不是简单的“暂停”,而是一种线程间的协作机制,让线程在特定条件下主动放弃锁并进入等待状态,直到其他线程显式唤醒它。
如果你对线程同步还停留在 synchronized 关键字的表面理解,那么深入学习 wait() 方法,将是你迈向高级并发编程的重要一步。
wait() 方法的基本语法与使用前提
wait() 是 java.lang.Object 类的实例方法,所有 Java 对象都可以调用。它的主要作用是让当前线程主动释放对象锁并进入等待状态,直到被其他线程通过 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
wait():无限期等待,直到被唤醒。wait(timeout):最多等待指定毫秒数,超时后自动恢复。wait(timeout, nanos):更精细的时间控制,通常不常用。
使用前提(必须满足三个条件)
-
必须在 synchronized 同步块或方法中调用
否则会抛出IllegalMonitorStateException。这是为了确保调用wait()的线程持有该对象的锁。 -
调用前必须持有对象的监视器(monitor)
也就是你必须已经通过synchronized获取了对象的锁。 -
wait() 会释放锁并进入等待队列
这是关键点——线程不会一直占用锁,而是“主动交出”锁,让其他线程有机会执行。
📌 比喻理解:把对象的锁想象成一个“会议室钥匙”。你拿着钥匙进会议室开会(执行同步代码),但突然发现资料没准备好。这时你调用
wait(),相当于你把钥匙放下,离开会议室,告诉别人:“我等资料好了再回来。” 其他人就可以拿钥匙进来开会了。
实际案例:生产者-消费者模型
让我们用经典的生产者-消费者问题来演示 wait() 方法的实际用法。
假设有一个共享的缓冲区,生产者往里面放数据,消费者从中取数据。如果缓冲区满了,生产者要等;如果为空,消费者也要等。
代码示例
public class ProducerConsumerExample {
private final Object lock = new Object(); // 用于同步的锁对象
private int buffer = 0; // 共享缓冲区
private boolean isEmpty = true; // 标记缓冲区是否为空
// 生产者线程
public class Producer implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
synchronized (lock) {
// 如果缓冲区已满,等待
while (!isEmpty) {
try {
System.out.println("生产者: 缓冲区已满,等待消费者消费...");
lock.wait(); // 释放锁,进入等待状态
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("生产者被中断");
return;
}
}
// 生产数据
buffer = i;
isEmpty = false;
System.out.println("生产者生产: " + buffer);
// 唤醒等待的消费者
lock.notifyAll();
}
}
}
}
// 消费者线程
public class Consumer implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
synchronized (lock) {
// 如果缓冲区为空,等待
while (isEmpty) {
try {
System.out.println("消费者: 缓冲区为空,等待生产者生产...");
lock.wait(); // 释放锁,进入等待状态
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("消费者被中断");
return;
}
}
// 消费数据
int value = buffer;
isEmpty = true;
System.out.println("消费者消费: " + value);
// 唤醒等待的生产者
lock.notifyAll();
}
}
}
}
// 启动测试
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Thread producerThread = new Thread(example.new Producer());
Thread consumerThread = new Thread(example.new Consumer());
producerThread.start();
consumerThread.start();
try {
producerThread.join();
consumerThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("生产消费流程结束");
}
}
代码详解
synchronized (lock):确保每次只有一个线程能进入临界区。while (!isEmpty):注意是while而不是if。为什么?- 因为线程被唤醒后,可能由于其他线程的干扰导致条件不再满足(即“虚假唤醒”)。
- 使用
while可以保证再次判断条件,避免错误执行。
lock.wait():释放锁并等待,直到被notifyAll()唤醒。lock.notifyAll():唤醒所有在该锁上等待的线程。推荐使用notifyAll(),而不是notify(),避免遗漏某个线程。
wait() 与 sleep() 的区别:你真的分得清吗?
很多初学者容易混淆 wait() 和 Thread.sleep()。虽然两者都能让线程“暂停”,但本质完全不同。
| 特性 | wait() | sleep() |
|---|---|---|
| 所属类 | Object 类 | Thread 类 |
| 是否释放锁 | ✅ 是 | ❌ 否 |
| 是否需要 synchronized | ✅ 必须 | ❌ 不需要 |
| 唤醒方式 | 通过 notify()/notifyAll() | 超时自动恢复 |
| 适用场景 | 线程协作、资源等待 | 简单延时、定时任务 |
📌 举个例子:
你正在排队买票,前面有 3 个人。
sleep(1000)就像你闭眼数 1 秒,但你还是站在队列里,别人不能插队。wait()就像你对售票员说:“我先去喝杯水,等我回来再买。” 然后你离开队伍,别人可以去排队买票。
所以,当你需要线程间协作时,永远优先考虑 wait()。
wait() 的常见误区与最佳实践
误区一:在非 synchronized 块中调用 wait()
// ❌ 错误写法
public void wrongMethod() {
Object obj = new Object();
obj.wait(); // 抛出 IllegalMonitorStateException
}
✅ 正确做法:必须在
synchronized块中调用。
误区二:使用 if 而不是 while 判断条件
// ❌ 危险写法
if (isEmpty) {
lock.wait(); // 可能虚假唤醒,导致误操作
}
✅ 正确做法:始终使用
while循环判断条件。
误区三:只用 notify() 而不用 notifyAll()
// ❌ 可能导致死锁
lock.notify(); // 只唤醒一个线程,另一个可能永远等不到
✅ 推荐:使用
notifyAll(),确保所有等待线程都有机会被唤醒。
wait() 方法的底层机制简析
wait() 方法在 JVM 层面依赖于监视器(monitor)机制。每个对象都有一个与之关联的监视器,线程进入 synchronized 块时获得该监视器,调用 wait() 时会:
- 释放当前持有的监视器锁;
- 将线程放入该对象的等待队列;
- 进入“WAITING”状态;
- 当收到
notify()或notifyAll()时,线程被移出等待队列,但不会立即恢复执行,需要重新竞争监视器锁。
这个过程保证了线程安全和协作的原子性。
如何安全地中断 wait() 线程?
有时候,我们需要在等待过程中中断线程。wait() 方法会抛出 InterruptedException,这是中断机制的体现。
示例:支持中断的 wait
try {
lock.wait(5000); // 最多等待 5 秒
} catch (InterruptedException e) {
// 处理中断
Thread.currentThread().interrupt(); // 重新设置中断标志
System.out.println("等待被中断,线程退出");
return;
}
📌 关键点:捕获
InterruptedException后,应调用Thread.currentThread().interrupt(),以便上层代码能感知中断状态。
总结:掌握 wait() 方法,打通多线程协作的任督二脉
Java Object wait() 方法 是 Java 并发编程的基石之一。它不仅仅是一个“暂停”工具,更是一种优雅的线程协作协议。通过合理使用 wait()、notify() 和 notifyAll(),我们可以构建出高效、安全的多线程应用。
- 理解其调用前提:必须在
synchronized块中。 - 掌握
while循环的使用,防止虚假唤醒。 - 区分
wait()与sleep()的本质差异。 - 善用
notifyAll()保证线程安全。 - 正确处理
InterruptedException。
当你能熟练运用 wait() 方法时,就标志着你已经从“会写线程”进阶到“懂线程协作”了。接下来,你可以学习 ReentrantLock、Semaphore 等更高级的并发工具,进一步提升你的并发编程能力。
最后提醒一句:线程安全,始于细节。每一个
wait()调用的背后,都是一次对程序逻辑的深度思考。