在 Java 多线程编程的世界里,wait() 和 notify() 是两个非常基础且至关重要的方法。你一定知道,多线程环境下处理共享数据时,协调线程的执行顺序往往是最让人头疼的问题。这两个方法正是 Java 为我们提供的解决这一难题的“原生钥匙”。它们定义在所有类的始祖 —— Object 类 中,这意味着任何 Java 对象都可以作为线程通信的锁载体。
在深入细节之前,让我们先聊聊它们诞生的背景。引入这两个方法的主要目的是为了摆脱“轮询”这种低效的编程模式。所谓的轮询,就是线程不断地重复检查某个条件是否满足(比如在 INLINECODE7ae114f6 循环中检查标志位)。这种做法虽然简单,但会极大地浪费 CPU 资源,导致“忙等待”。而在实际开发中,我们更希望线程在条件不满足时能够沉睡,不再占用 CPU,直到条件成熟被其他线程唤醒。这正是 INLINECODE5e482664 和 notify() 大显身手的地方。
wait() 方法详解
INLINECODE471f169b 方法是 java.lang.Object 类的一部分。当某个线程调用了对象的 INLINECODEe4c77d6e 方法后,它会立即释放当前持有的对象锁,并停止当前的执行流程,进入等待状态。在这个状态下,它会一直等待,直到其他线程调用了同一个对象的 INLINECODE55be6eeb 或 INLINECODE929abe29 方法来唤醒它,或者等待时间超时。
wait() 方法主要有 3 种重载形式:
#### 1. wait()
这是 INLINECODE8f69ea9b 方法最基础的形式,它不接受任何参数。调用它会导致当前线程无限期地等待,直到其他线程调用了 INLINECODE2ad88d3d 或 notifyAll() 方法。
public final void wait() throws InterruptedException
#### 2. wait(long timeout)
这个版本接受一个毫秒级的超时参数。调用它会导致线程等待,直到被通知唤醒,或者直到超过了指定的超时时间(以先发生者为准)。这种机制可以有效避免线程因为逻辑错误而永久死锁。
public final void wait(long timeout) throws InterruptedException
#### 3. wait(long timeout, int nanos)
这个版本提供了更高精度的控制,它允许你指定毫秒之外的纳秒级时间。虽然在实际 JVM 调度中,纳秒级的精度很难完全保证,但在某些对时间敏感的场景下,它提供了理论上的更高精度。
public final void wait(long timeout, int nanos) throws InterruptedException
notify() 与 notifyAll() 方法
notify() 方法同样定义在 Object 类中。它的作用是唤醒正在等待该对象(锁)的某一个线程。需要注意的是,它具体唤醒哪一个等待线程是由 JVM 调度器随机选择的,我们无法人为指定。
public final void notify()
图解:展示了线程如何进入等待集,以及如何通过通知被唤醒并重新竞争锁的过程。
此外,还有一个 INLINECODE53b9c4e5 方法。它不是唤醒某一个线程,而是唤醒所有正在等待该对象锁的线程。虽然这听起来更公平,但也会导致所有被唤醒的线程瞬间竞争锁,可能引发“惊群效应”,增加系统开销。通常建议优先使用 INLINECODE2510471c,除非你确实需要唤醒所有等待线程来处理不同的条件。
wait() 和 notify() 的核心差异
为了更清晰地理解这两个方法,让我们通过下面的表格来梳理一下它们之间的核心差异:
Wait()
—
功能:导致当前线程释放锁并进入等待状态。
锁状态:线程调用 wait() 后,会立即释放持有的监视器锁。
执行顺序:线程从“运行中”变为“等待中”。
所属类:定义在 java.lang.Object 类中。
用途:主要用于线程间的通信或条件等待。
异常:必须捕获 InterruptedException。
实战代码示例:生产者与消费者模型
光说不练假把式。让我们通过一个真实的代码示例来演示 INLINECODE484ea25c 和 INLINECODEdc432357 的用法。为了让你看得更明白,我们编写一个经典的“生产者-消费者”场景:两个线程共享一个队列,一个负责存入数据(生产者),另一个负责取出数据(消费者)。当队列满时,生产者等待;当队列空时,消费者等待。
#### 示例 1:基础协作演示
在这个例子中,我们将模拟一个简单的场景:线程 2 (t2) 必须等待线程 1 (t1) 完成某项初始化工作后才能继续执行。
// 演示 wait() 和 notify() 基础用法的 Java 程序
class SharedResource {
// 使用 volatile 关键字防止线程缓存,确保可见性
// 这是一个简单的状态标志,表示“第一部分工作是否已完成”
volatile boolean part1Done = false;
// 同步方法 part1:由线程 t1 调用
synchronized void part1() {
System.out.println("-> [t1] 正在执行初始化工作...");
// 模拟耗时工作
try { Thread.sleep(1000); } catch (InterruptedException e) {}
part1Done = true; // 标记工作已完成
System.out.println("-> [t1] 工作完成,准备通知等待的线程...");
// 通知正在该对象上等待的线程(如果有)
// 注意:当前线程 t1 仍持有锁,直到此方法结束
notify();
System.out.println("-> [t1] 通知已发送,即将释放锁。");
}
// 同步方法 part2:由线程 t2 调用
synchronized void part2() {
// 循环检查:防止“虚假唤醒”(Spurious Wakeup)
// 这是一个非常重要的最佳实践
while (!part1Done) {
try {
System.out.println("-> [t2] 检测到条件不满足,进入等待状态...");
// 释放锁并等待,直到被 notify() 唤醒
wait();
System.out.println("-> [t2] 收到通知!被唤醒并重新获取到锁。");
}
catch (InterruptedException e) {
System.out.println("线程被异常中断");
}
}
// 此时 part1Done 为 true,可以安全继续执行
System.out.println("-> [t2] 检测到条件已满足,开始执行后续任务...");
}
}
public class Main {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
// 创建线程 t1,负责执行初始化
Thread t1 = new Thread(() -> resource.part1());
// 创建线程 t2,依赖 t1 的结果
Thread t2 = new Thread(() -> resource.part2());
// 我们故意先启动 t2,演示它如何等待
t2.start();
// 稍作延迟确保 t2 先运行并进入 wait
try { Thread.sleep(100); } catch (InterruptedException e) {}
t1.start();
}
}
输出结果:
-> [t2] 检测到条件不满足,进入等待状态...
-> [t1] 正在执行初始化工作...
-> [t1] 工作完成,准备通知等待的线程...
-> [t1] 通知已发送,即将释放锁。
-> [t2] 收到通知!被唤醒并重新获取到锁。
-> [t2] 检测到条件已满足,开始执行后续任务...
从输出中我们可以看到,t2 启动后因为条件未满足而进入 INLINECODEe3c94c3f 状态,释放了锁。随后 t1 获取锁,完成工作并调用 INLINECODE6a8b2fe2。只有当 t1 执行完同步块释放锁后,t2 才真正从 wait() 返回并继续执行。
#### 示例 2:生产者与消费者(解决实际问题)
让我们看一个更复杂的例子,模拟队列的存取操作,这更接近实际开发中的场景。
import java.util.LinkedList;
import java.util.Queue;
// 这是一个简单的线程安全队列实现
class SharedBuffer {
private Queue buffer = new LinkedList();
private int capacity = 5; // 队列最大容量
// 生产者方法:向队列存入数据
public void produce(int item) throws InterruptedException {
synchronized (this) {
// 如果队列满了,生产者必须等待
while (buffer.size() == capacity) {
System.out.println("[生产者] 队列已满,等待消费者取走数据...");
wait(); // 释放锁,等待消费者 notify
System.out.println("[生产者] 被唤醒,再次尝试存入。");
}
buffer.add(item);
System.out.println("[生产者] 生产了: " + item);
// 通知正在等待的消费者(队列里有东西了)
// 这里使用 notifyAll 更安全,防止唤醒了另一个生产者导致死锁
notifyAll();
}
}
// 消费者方法:从队列取出数据
public void consume() throws InterruptedException {
synchronized (this) {
// 如果队列空了,消费者必须等待
while (buffer.size() == 0) {
System.out.println("[消费者] 队列为空,等待生产者放入数据...");
wait(); // 释放锁,等待生产者 notify
System.out.println("[消费者] 被唤醒,再次尝试取货。");
}
int item = buffer.poll();
System.out.println("[消费者] 消费了: " + item);
// 通知正在等待的生产者(队列里有空位了)
notifyAll();
}
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
SharedBuffer buffer = new SharedBuffer();
// 创建生产者线程
Thread producerThread = new Thread(() -> {
try {
for (int i = 1; i {
try {
for (int i = 1; i <= 10; i++) {
buffer.consume();
Thread.sleep(300); // 模拟消费慢一点
}
} catch (InterruptedException e) { e.printStackTrace(); }
});
producerThread.start();
consumerThread.start();
}
}
在这个例子中,我们使用了 INLINECODE9bc23f9f。为什么不用 INLINECODE086381d3?因为可能有两个生产者线程同时等待。如果我们使用 notify(),可能会错误地唤醒另一个生产者,而另一个生产者发现队列还是满的,会再次等待,这样就没人能唤醒消费者了,导致死锁。
常见异常与处理
在使用这些方法时,有几个陷阱是我们必须小心的:
1. 必须在同步上下文中调用
这是新手最容易犯的错误。INLINECODE74ac8d9e, INLINECODEed2b80cc, 和 INLINECODE4a4ce958 必须在当前线程持有该对象锁(即在 INLINECODE5c3d7f61 块或方法中)的情况下调用。否则,JVM 会抛出 IllegalMonitorStateException。
// 错误示例
public void wrongMethod() {
Object obj = new Object();
obj.wait(); // 抛出 IllegalMonitorStateException,因为没持有锁
}
2. 处理 InterruptedException
INLINECODEf8ab66ab 方法会抛出 INLINECODE07943fb6。这表示线程在等待期间被其他线程中断了。作为最佳实践,我们不应该吞掉这个异常,而应该处理它,通常是通过再次设置中断标志或者向上层抛出。
3. 虚假唤醒
你可能在上面的代码中注意到了,我们在 INLINECODE7f669190 循环中调用 INLINECODE13be43cc,而不是用 if 语句。
// 推荐做法
while (conditionNotMet) {
wait();
}
这是为什么呢?因为操作系统底层允许所谓的“虚假唤醒”,即线程在没有收到 INLINECODEf9c39b46 的情况下也能从 INLINECODE1e897473 返回。如果不放在循环里再次检查条件,程序可能会在条件未真正满足时继续向下执行,导致难以排查的逻辑错误。
性能优化与最佳实践
在实际的高并发开发中,直接使用 INLINECODE10481ea9 和 INLINECODE8ccf279b 往往比较繁琐且容易出错。我们可以考虑以下优化方向:
- 优先使用并发工具包:在 Java 5+ 中,INLINECODEdb0cc539 包提供了更高级的工具,如 INLINECODE7c195e45(上面示例的完美替代者)、INLINECODEdd6be4eb、INLINECODE6bfe36cb 等。它们封装了
wait/notify逻辑,使用起来更安全、更直观。
- 减少锁的粒度:
synchronized块的范围应尽可能小。例如,在获取数据之前做不需要锁的准备工作,只在必要时才进入同步块。
- notify() vs notifyAll():默认情况下,如果你的等待线程有多个不同类型(比如消费者和生产者都在等),或者逻辑比较复杂,使用 INLINECODEd00d24fb 更安全。只有在确定只有一个等待线程或者所有等待线程都处理相同任务时,才使用 INLINECODEa3343f90 来减少上下文切换的开销。
总结
在这篇文章中,我们深入探讨了 Java 中线程间通信的核心机制 —— INLINECODE7db304ab 和 INLINECODE942c4aa7。我们不仅了解了它们的基本用法,还通过代码示例看到了它们在实际场景(如生产者-消费者模型)中的应用,以及如何正确处理异常和“虚假唤醒”问题。
掌握这些底层的同步机制对于编写高性能、线程安全的多线程 Java 程序至关重要。虽然在实际业务代码中我们可能会更多地使用 INLINECODE6241e240 或 INLINECODEeab8d6d4 等高级封装,但理解其背后的原理,能让你在面对复杂的并发问题时游刃有余。
希望这篇文章能帮助你彻底搞懂 Java 线程通信的奥秘!下次当你遇到多线程协作的问题时,不妨试着画个图,理清楚谁在等、谁在叫,你会发现它其实并不复杂。