Java案例:如何唤醒等待线程?——从原理到实战的完整指南
目录导读
- 线程等待与唤醒的核心机制
- Object类的wait/notify/notifyAll方法详解
- 实战案例:生产者-消费者模型中的线程唤醒
- Condition接口与Lock的灵活唤醒
- 常见陷阱与最佳实践
- Q&A高频问题解答
线程等待与唤醒的核心机制
在Java多线程编程中,线程等待与唤醒是解决协同问题的关键,当某个线程需要等待特定条件(如队列为空、资源未就绪)时,它应该释放锁并进入等待状态;当条件满足时,其他线程需要通知它继续执行,Java提供了两种主要机制:

- Object类的wait/notify/notifyAll(基于synchronized)
- Condition接口(基于ReentrantLock)
核心问题:一个线程调用了
wait()后,如何被重新唤醒?答案是:必须由另一个线程在持有同一把锁的前提下调用notify()或notifyAll()。
Object类的wait/notify/notifyAll方法详解
1 方法签名与前提条件
public final void wait() throws InterruptedException public final void notify() public final void notifyAll()
前提条件:调用这些方法的线程必须持有该对象的监视器锁(即必须在synchronized块或方法内),否则会抛出IllegalMonitorStateException。
2 执行流程
- 线程A调用
obj.wait():- 释放对象锁
- 进入
WAITING状态,加入该对象的等待集(Wait Set)
- 线程B调用
obj.notify():- 随机唤醒等待集中的一个线程(非公平)
- 被唤醒的线程从
WAITING转为BLOCKED状态,尝试重新竞争锁
- 线程C调用
obj.notifyAll():唤醒等待集中的所有线程,它们同时竞争锁
3 伪代码演示
synchronized (sharedObj) {
while (!condition) {
sharedObj.wait(); // 释放锁,进入等待
}
// 执行条件满足后的逻辑
}
注意:必须使用while循环检查条件,防止虚假唤醒(Spurious Wakeup)。
实战案例:生产者-消费者模型中的线程唤醒
场景描述
一个仓库最多存储10件商品,当仓库满时,生产者线程等待;当仓库空时,消费者线程等待,双方通过notifyAll相互唤醒。
代码实现
class Warehouse {
private final int capacity = 10;
private final List<String> items = new ArrayList<>();
public synchronized void produce(String item) throws InterruptedException {
while (items.size() == capacity) {
System.out.println("仓库已满,生产者等待...");
wait();
}
items.add(item);
System.out.println("生产了 " + item + ",库存:" + items.size());
notifyAll(); // 唤醒可能等待的消费者
}
public synchronized String consume() throws InterruptedException {
while (items.isEmpty()) {
System.out.println("仓库为空,消费者等待...");
wait();
}
String item = items.remove(0);
System.out.println("消费了 " + item + ",库存:" + items.size());
notifyAll(); // 唤醒可能等待的生产者
return item;
}
}
关键点
notifyAll()比notify()更安全,避免“信号丢失”导致死等- 使用
while循环而非if检查条件,防止虚假唤醒破坏数据一致性
Condition接口与Lock的灵活唤醒
1 为什么需要Condition?
synchronized的wait/notify只能有一个等待集,难以实现精准唤醒(例如只唤醒生产者或只唤醒消费者)。ReentrantLock配合Condition可以创建多个等待集。
2 使用示例:用两个Condition实现精准唤醒
class BetterWarehouse {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 生产者等待条件
private final Condition notEmpty = lock.newCondition(); // 消费者等待条件
private int count = 0;
private final int capacity = 10;
public void produce() throws InterruptedException {
lock.lock();
try {
while (count == capacity) {
notFull.await(); // 生产者等待
}
count++;
System.out.println("生产后库存:" + count);
notEmpty.signal(); // 只唤醒一个消费者
} finally {
lock.unlock();
}
}
public void consume() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 消费者等待
}
count--;
System.out.println("消费后库存:" + count);
notFull.signal(); // 只唤醒一个生产者
} finally {
lock.unlock();
}
}
}
优势:
- 避免“惊群效应”(所有线程被唤醒却只有少数能竞争到锁)
- 代码语义更清晰:
notFull和notEmpty分别控制两类线程
常见陷阱与最佳实践
1 虚假唤醒(Spurious Wakeup)
即使没有其他线程调用notify,等待的线程也可能被操作系统唤醒。必须用while循环重试条件。
2 信号丢失(Lost Signal)
- 错误用法:在
if中调用wait,条件变化后未被检查 - 解决方法:始终使用
while(!condition)
3 死锁与饥饿
- 死锁:线程A持有锁X等待条件Y,线程B持有锁Y等待条件X
- 饥饿:优先级低的线程长期得不到锁,可尝试公平锁
4 性能优化
- 优先使用
ReentrantLock + Condition替代synchronized,尤其是需要超时等待、公平锁、多个条件时 - 避免在循环中频繁调用
wait,确保条件判断高效
Q&A高频问题解答
Q1:notify()和notifyAll()应该选哪个?
A:优先选notifyAll(),虽然notify()性能稍好,但它随机唤醒一个线程,可能导致信号丢失(唤醒的线程不满足条件而继续等待),除非你能确定所有等待线程条件相同,否则用notifyAll更安全。
Q2:wait()为什么要放在同步块中?
A:确保时序正确性,如果不在同步块中,可能出现:
- 线程A检查条件满足,但此时线程B修改了条件
- 线程A调用
wait,却可能永远收不到通知(通知已经发出)
同步块保证了检查条件和调用wait的原子性。
Q3:Condition的await()和sleep()有什么区别?
A:
await()释放锁并等待,允许其他线程获取锁;sleep()不释放锁await()可以被中断;sleep()也可以被中断但会抛出InterruptedExceptionawait()需要与Condition和Lock配合使用
Q4:如何唤醒指定线程?
A:Java本身不提供直接唤醒指定线程的API,但可通过:
- 使用
Condition的signal()只会唤醒一个等待线程(无法指定具体哪个) - 使用标志变量+
while循环,让线程自行决定是否继续等待 - 使用
Semaphore或CountDownLatch等并发工具实现“点到点”唤醒
Q5:如果wait()时线程被中断会怎样?
A:线程会从等待状态立即返回,并抛出InterruptedException,建议在异常处理中重置中断状态:Thread.currentThread().interrupt()。
唤醒等待线程是Java并发编程的核心技能,通过本文的案例,你应当掌握:
synchronized + wait/notify的基础用法ReentrantLock + Condition的精准控制- 使用
while循环应对虚假唤醒 - 在生产-消费者等典型场景中避免死锁和信号丢失
最佳实践:新版代码优先使用java.util.concurrent包提供的类(如BlockingQueue),它们封装了等待/唤醒逻辑,能显著降低编码复杂度,但理解底层机制,仍是排查复杂并发问题的关键。