在Java并发编程中,wait()
, notify()
, 和 notifyAll()
是用于实现线程间精确协作的核心机制。它们允许线程在特定条件未满足时主动等待,并在条件可能满足时被唤醒,从而高效地协调工作。下面这张表格帮助你快速掌握它们的特点。
特性 | wait() | notify() | notifyAll() |
---|---|---|---|
所属类 | Object | Object | Object |
作用 | 让当前线程释放锁并进入等待 | 唤醒一个在该对象上等待的线程 | 唤醒所有在该对象上等待的线程 |
锁处理 | 释放当前持有的对象锁 | 不释放锁,需等同步块执行完毕 | 不释放锁,需等同步块执行完毕 |
调用前提 | 必须在同步代码块(synchronized) 内调用 | 必须在同步代码块(synchronized) 内调用 | 必须在同步代码块(synchronized) 内调用 |
线程状态 | 进入 WAITING 或 TIMED_WAITING | 被唤醒的线程进入 BLOCKED,竞争锁 | 所有被唤醒的线程进入 BLOCKED,竞争锁 |
🔍 理解核心机制
要正确使用这三个方法,需要理解其背后的几个关键点。
-
与对象锁绑定 :这三个方法的调用都依赖于对象的内置锁(监视器锁) 。线程在调用它们之前,必须 先获得该对象的锁,这就是为什么它们必须在
synchronized
代码块或方法内使用,否则会抛出IllegalMonitorStateException
。 -
等待与唤醒的条件检查 :一个至关重要的实践是,线程被唤醒后,不应该立即假定其等待的条件已经满足。这是因为:
-
虚假唤醒 :在某些情况下,即使没有线程调用
notify
,等待的线程也可能被唤醒。 -
通知的泛化 :
notify()
是随机唤醒一个线程,它可能并非在等待当前已满足的条件。因此,线程被唤醒后,必须使用循环重新检查等待条件。如果条件不满足,应继续等待 。
scsssynchronized (sharedLock) { while (!conditionIsMet) { // 必须用循环检查条件 sharedLock.wait(); } // ... 条件满足后执行任务 }
-
-
为什么定义在Object类中 :这是因为这些方法操作的是对象的同步锁 。每个对象都有一把锁,
wait()
方法会让当前线程释放它正持有的这个对象 的锁,而notify()
则是唤醒其他正在等待这个对象 的锁的线程。锁是对象级别的,所以这些方法自然定义在Object
类中,而非Thread
类 。
⚖️ wait() 与 sleep() 的抉择
Thread.sleep()
也能让线程暂停,但它与 wait()
有本质区别,具体对比如下 :
特性 | wait() | sleep() |
---|---|---|
所属类 | Object | Thread |
锁的行为 | 释放已持有的对象锁 | 不释放任何锁 |
使用场景 | 线程间协作,等待特定条件 | 单纯暂停当前线程的执行 |
唤醒方式 | 需被 notify/notifyAll 唤醒或超时 |
在指定时间后自动恢复 |
调用要求 | 必须在 synchronized 块内 |
可以在任何地方调用 |
简单来说,当需要让线程等待某个条件并允许其他线程获取锁时,用 wait()
;当只是想暂时让线程休眠片刻,且不希望影响其他线程获取锁时,用 sleep()
。
🛠️ 项目实战:生产者-消费者模型
生产者-消费者问题是一个经典的多线程协作案例。假设有一个共享的、容量有限的仓库,生产者负责向仓库添加产品,消费者负责从仓库取走产品。规则是:仓库满时生产者等待,仓库空时消费者等待 。
以下是使用 wait()
和 notifyAll()
实现的核心代码:
java
import java.util.LinkedList;
public class Warehouse {
private final int MAX_SIZE = 10; // 仓库最大容量
private final LinkedList<Object> items = new LinkedList<>(); // 存储产品的链表
// 生产方法
public synchronized void produce(Object product) throws InterruptedException {
while (items.size() >= MAX_SIZE) {
// 仓库已满,生产者线程等待
wait();
}
items.add(product); // 生产产品
System.out.println("生产了一个产品,当前库存: " + items.size());
notifyAll(); // 通知可能正在等待的消费者(或其它生产者)
}
// 消费方法
public synchronized Object consume() throws InterruptedException {
while (items.isEmpty()) {
// 仓库为空,消费者线程等待
wait();
}
Object product = items.removeFirst(); // 消费产品
System.out.println("消费了一个产品,当前库存: " + items.size());
notifyAll(); // 通知可能正在等待的生产者(或其它消费者)
return product;
}
}
代码要点解析 :
- 条件循环检查 :在
produce
和consume
方法中,都使用了while
循环来判断条件(库存已满或为空),而不是if
语句。这是为了防止虚假唤醒,确保线程被唤醒后条件确实满足才继续执行。 - 使用
notifyAll()
:这里选择使用notifyAll()
是因为有生产者和消费者两类线程在等待。如果使用notify()
,可能会错误地只唤醒另一个生产者(当仓库已满时),而真正需要被唤醒的消费者则可能一直得不到执行,导致死锁 。notifyAll()
会唤醒所有等待的线程,让它们去竞争锁并重新检查条件,从而更安全 。
在 Spring Boot 项目中,除了经典的生产者-消费者模式,wait()
和 notify()
/notifyAll()
机制还可以应用于多种需要精细线程协作的场景。下面通过一个表格汇总一些典型的使用案例。
应用场景 | 核心目标 | 关键协作机制 |
---|---|---|
资源池管理 (如数据库连接池) | 控制资源的使用,当资源耗尽时,新请求等待;资源释放时,通知等待线程。 | 获取资源时若无可用量,则 wait() ;释放资源时,调用 notifyAll() 唤醒等待线程。 |
异步任务结果同步 | 主线程提交异步任务后,等待工作线程处理完毕并返回结果。 | 主线程提交任务后 wait() ;工作线程完成后 notify() 或 notifyAll() 并设置结果。 |
自定义定时任务调度 | 实现一个调度器,让任务在特定时间点执行或按固定间隔周期性执行。 | 调度线程计算下一个执行时间点并 wait(timeout) ;任务添加或取消时 notify() 重新计算。 |
系统初始化屏障 | 确保多个核心服务完成初始化后,应用才正式对外提供服务。 | 启动线程在各服务初始化前 wait() ;每个服务初始化完成后检查并调用 notifyAll() 。 |
🛠️ Spring Boot 项目实战案例
以下是一个简化的 异步任务结果同步 示例,模拟在 Spring Boot 中处理一个耗时操作。
typescript
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class AsyncTaskProcessor {
// 用于存储任务结果和关联的锁对象
private final ConcurrentHashMap<String, TaskResult> taskResults = new ConcurrentHashMap<>();
// 提交异步任务的方法
public void submitTask(String taskId, Runnable task) {
// 初始化任务结果和锁对象
taskResults.put(taskId, new TaskResult());
// 启动异步线程执行任务
new Thread(() -> {
// 模拟耗时操作
try {
Thread.sleep(2000);
// 任务完成,设置结果并通知
synchronized (this) {
TaskResult result = taskResults.get(taskId);
result.setData("Task " + taskId + " completed!");
result.setDone(true);
this.notifyAll(); // 通知所有等待此任务结果的线程
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
// 获取任务结果的方法(阻塞直到任务完成)
public String getTaskResult(String taskId) throws InterruptedException {
TaskResult result = taskResults.get(taskId);
if (result == null) {
throw new IllegalArgumentException("Task ID not found: " + taskId);
}
synchronized (this) {
while (!result.isDone()) {
this.wait(); // 等待任务完成的通知
}
return result.getData();
}
}
// 内部类,封装任务结果
private static class TaskResult {
private String data;
private boolean isDone = false;
// Getter and Setter ...
public String getData() { return data; }
public void setData(String data) { this.data = data; }
public boolean isDone() { return isDone; }
public void setDone(boolean done) { isDone = done; }
}
}
代码要点解析:
- 异步执行 :
submitTask
方法会立即返回,不会阻塞调用线程,耗时任务在后台执行。 - 结果等待 :
getTaskResult
方法会检查任务是否完成。如果未完成,调用线程会在wait()
处阻塞。 - 协作机制 :当后台任务线程执行完毕,会设置结果标记,并调用
notifyAll()
唤醒所有正在等待该任务结果的线程。被唤醒的线程会再次检查isDone
条件,如果为真则获取结果。 - 锁对象 :此例中使用
this
(即AsyncTaskProcessor
实例本身)作为锁对象,确保wait
和notifyAll
作用于同一个监视器。
⚠️ 重要注意事项与最佳实践
- 循环条件检查 :务必在
while
循环中检查等待条件,而不是if
语句,以防止虚假唤醒(spurious wakeup)。 - 优先考虑
notifyAll()
:除非你能百分之百确定只有一个线程在等待,且唤醒它总是正确的,否则优先使用notifyAll()
更为安全,可以避免某些线程被永久遗忘(线程饥饿)。 - 考虑使用 Java.util.concurrent 工具包 :对于复杂的生产-消费场景或线程协作,Java 5 引入的
java.util.concurrent
包提供了更高级、更安全的工具,如BlockingQueue
(阻塞队列)。使用ArrayBlockingQueue
可以轻松实现生产者-消费者模式,而无需手动处理wait/notify
的底层细节,大大简化了代码并降低了出错风险 。 - 与 Spring 框架的集成 :在 Spring Boot 中,可以利用
@Async
注解、事件监听机制或消息中间件(如 RabbitMQ, Kafka)来实现更解耦、更易于管理的异步处理和线程间通信,这往往是比直接使用底层wait/notify
更符合 Spring 设计哲学的做法。
💎 总结
wait()
, notify()
, 和 notifyAll()
是 Java 底层线程协作的强大工具。理解其机制并遵循"循环检查条件 "和"谨慎选择通知方式 "的原则,是编写正确、健壮的多线程程序的关键。对于现代并发开发,积极了解并使用 java.util.concurrent
包中的高级组件(如 Lock
, Condition
, 各种阻塞队列等)通常是更优的选择,它们提供了更好的灵活性和可控性。
在 Spring Boot 项目中,wait()
和 notify()
/notifyAll()
的用武之地在于那些需要精细控制线程等待与唤醒的场景。虽然实际开发中我们更倾向于使用更高级的并发工具或框架特性,但理解这些底层机制的原理,对于设计健壮的并发模块、排查复杂的线上问题依然至关重要。