wait方法是Java多线程开发中最基础也最容易用错的API之一。不少人在第一次接触wait时,会把它简单理解成让线程睡一会儿,结果运行时抛出IllegalMonitorStateException,或者线程被唤醒后业务逻辑却出现了错乱。要正确使用wait,需要理解它背后的监视器锁机制、条件判断方式以及唤醒语义。本文从常见误区入手,逐一拆解wait的正确用法。

误区一:不加锁就直接调用wait
wait方法并不是Thread类的方法,而是定义在Object类中的方法。它的核心作用是让当前线程释放所持有的监视器锁,并进入该对象的等待队列,直到其他线程调用同一个对象的notify或notifyAll将其唤醒。这个机制决定了wait必须在一个已经获得锁的环境下调用,否则JVM无法知道该释放哪个锁。
最常见的错误写法是这样的:
public class WrongWait {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
// 没有获取lock的监视器锁就直接调用wait,运行时抛异常
lock.wait();
}
}上面这段代码在运行时会直接抛出java.lang.IllegalStateException的近亲:java.lang.IllegalMonitorStateException,异常信息通常是current thread is not owner。正确做法是把wait放进synchronized块中,并且synchronized锁的对象必须与调用wait的对象完全一致:
synchronized (lock) {
while (!condition) {
lock.wait(); // 正确:当前线程已持有lock的锁
}
// 条件满足后执行业务逻辑
}还有一个隐蔽的坑:锁对象和wait对象不一致。比如写成synchronized(this)却调用otherObject.wait(),同样会抛出IllegalMonitorStateException。排查这类问题时,优先检查synchronized括号里的对象和调用wait的对象是不是同一个引用。
误区二:用if判断条件而不是while
wait被唤醒后,线程会从wait调用处继续往下执行,但此时条件不一定真的满足了。原因有两个:第一,notify只是随机唤醒一个等待线程,被唤醒的线程需要重新竞争锁,等到它真正拿到锁时,条件可能已经被其他线程改掉了,这被称为虚假唤醒之外的正常竞争失效;第二,JVM规范本身允许虚假唤醒发生,即没有任何线程调用notify,等待线程也可能自己醒来。
因此,wait的标准姿势是放在while循环中,醒来后重新检查条件:
synchronized (lock) {
while (queue.isEmpty()) { // 用while而不是if
lock.wait();
}
// 走到这里说明队列一定不为空
String item = queue.remove();
}如果写成if,一旦发生条件被抢先修改或者虚假唤醒,线程会直接往下执行,取出数据时就会抛出IndexOutOfBoundsException之类的异常,或者处理到脏数据。这类问题在测试环境很难复现,往往在高并发的生产环境才爆发,排查成本非常高。JDK官方文档也明确建议wait永远放在循环中调用。
误区三:混淆wait和sleep,误用notify和notifyAll
wait和sleep都能让线程暂停,但它们有本质区别。wait是Object的方法,必须在同步块中调用,会释放锁;sleep是Thread的静态方法,可以在任意位置调用,不会释放已经持有的锁。用一个表格对比更清晰:
| 对比项 | wait | sleep |
|---|---|---|
| 所属类 | Object | Thread |
| 是否释放锁 | 释放监视器锁 | 不释放 |
| 调用前提 | 必须持有锁 | 无要求 |
| 唤醒方式 | notify/notifyAll/超时 | 超时自动恢复 |
误用sleep做线程间协调是最常见的错误之一。比如线程A持有锁后sleep了5秒,其他线程全部被阻塞,整个系统吞吐量急剧下降。正确的协调手段应该是wait配合notify。
notify和notifyAll的选择同样重要。notify只会随机唤醒一个等待线程,如果等待队列中有多种不同条件的线程在等,被唤醒的可能根本不是你想要的那个,最终导致信号丢失、任务卡死。在不确定的情况下,优先使用notifyAll,让所有等待线程都醒来重新检查条件,条件不满足的会再次进入等待,虽然多消耗一点CPU,但逻辑上是安全的。
完整示例:生产者消费者模式
把前面的要点串起来,一个线程安全的阻塞队列生产者消费者示例如下:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumer {
private static final Queue<Integer> queue = new LinkedList<>();
private static final int CAPACITY = 5;
private static final Object lock = new Object();
static class Producer extends Thread {
public void run() {
int value = 0;
while (true) {
synchronized (lock) {
while (queue.size() == CAPACITY) { // 队列满则等待
try {
lock.wait();
} catch (InterruptedException e) {
return;
}
}
queue.offer(value++);
System.out.println("生产: " + (value - 1) + " 当前大小: " + queue.size());
lock.notifyAll(); // 唤醒消费者
}
}
}
}
static class Consumer extends Thread {
public void run() {
while (true) {
synchronized (lock) {
while (queue.isEmpty()) { // 队列空则等待
try {
lock.wait();
} catch (InterruptedException e) {
return;
}
}
int value = queue.poll();
System.out.println("消费: " + value + " 剩余: " + queue.size());
lock.notifyAll(); // 唤醒生产者
}
}
}
}
public static void main(String[] args) {
new Producer().start();
new Consumer().start();
}
}这段代码体现了三个关键点:所有对队列的读写都包裹在synchronized中;wait一律放在while循环里;状态变化后使用notifyAll唤醒全部相关线程。另外注意wait会响应中断,抛出InterruptedException时应该正确处理,比如恢复中断标记或者退出循环,直接吞掉异常是不好的实践。
如果觉得手写wait和notify过于繁琐,实际项目中更推荐使用java.util.concurrent包提供的BlockingQueue、Condition配合Lock等工具,它们封装了等待通知语义,代码更简洁,也不容易出错。但理解wait的底层机制依然是必要的,因为Condition的await和signal本质上就是wait和notify的升级版。