wait() 调用时为何必须持有锁
wait() 方法必须在 synchronized 块中调用,是因为它需要原子性地释放当前对象锁并进入等待状态。如果不在同步块内,线程在检查条件后、调用 wait() 前可能被其他线程修改了数据,导致死锁或丢失信号。jvm 要求调用 wait() 的线程必须拥有该对象的监视器锁,否则抛出 illegalmonitorstateexception。

public class Counter {
private int count = 0;
public void waitAndReset() throws InterruptedException {
synchronized (this) {
while (count > 0) {
this.wait(); // 原子性释放锁并挂起
}
count = 0;
}
}
}
这段代码展示了标准的用法:先获取锁,再在循环中检查条件,满足条件则调用 wait()。如果不加 synchronized,代码会在运行时直接崩溃。
为什么需要 while 循环而非 if
使用 while 循环包裹 wait() 是为了防止虚假唤醒(Spurious Wakeup)和竞态条件。当线程被唤醒时,条件可能已经不再成立,因为可能有多个线程同时被唤醒,或者在唤醒到重新获取锁之间状态发生了改变。因此,每次唤醒后都必须重新检查条件,确保只有条件真正满足时才继续执行后续逻辑。

public void safeWait() throws InterruptedException {
synchronized (this) {
// 错误示范:if 只检查一次,唤醒后可能条件已变
// if (condition) { wait(); }
// 正确做法:循环检查
while (!condition) {
wait();
}
// 此时条件一定为真
}
}
这个对比展示了为什么 if 是不安全的。在多线程环境下,notifyAll() 可能会唤醒所有等待线程,但只有一个线程能成功处理任务,其他线程醒来后发现资源已被占用,必须再次进入等待。只有 while 结构能保证这一点。
基于 notify() 与 notifyAll() 的具体问题
在使用 notify() 时,如果存在多个不同类型的等待线程,随机唤醒一个可能导致死锁或逻辑错误。例如,生产者-消费者模型中,如果只通知一个线程,而恰好唤醒了另一个同类型的等待者,可能无法推进业务。因此,除非确定只有一个等待线程,否则应优先使用 notifyAll() 来确保所有相关线程都能重新评估条件。
public void notifyAllExample() {
synchronized (this) {
// 修改状态
dataReady = true;
// 唤醒所有等待线程,让它们竞争锁并重新检查条件
this.notifyAll();
}
}
这里的关键在于,notifyAll() 只是将线程从 WAITING 状态转为 RUNNABLE,它们仍需竞争锁。一旦获得锁,就会回到 while 循环中重新判断条件,从而保证安全性。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











