文章目录
为什么wait()和notify()必须在同步块中?
大家好,我是闫工,今天又要来和大家聊一聊Java多线程中的一个经典问题:为什么wait()和notify()必须在同步块中? 这个问题看似简单,但背后涉及到Java内存模型、锁机制以及线程安全的核心原理。作为一个资深的Java工程师,我必须要用最通俗易懂的语言,带大家深入理解这个问题的本质。

引言
多线程编程一直是Java世界的重头戏,而多线程编程中又绕不开一个话题——等待与通知(wait()和notify())。这两个方法就像是多线程世界中的“红绿灯”,控制着线程的运行节奏。但是,如果你不把它们放在同步块里使用,那可就要小心了,这不仅会让代码变得不可预测,甚至会引发严重的程序错误。
今天的文章中,我会以一个轻松幽默的方式带大家理解这个问题,从历史背景、核心原理到实际应用误区,全面解析为什么wait()和notify()必须在同步块中。
一、历史的回响:Java设计者的初衷
1. Java多线程机制的发展
要理解为什么wait()和notify()必须在同步块中使用,我们得先回顾一下Java多线程机制的发展历程。Java从诞生之初就内置了对多线程的支持,这在当时是一个非常创新的设计。然而,正如任何新技术一样,早期的多线程编程也充满了挑战。
2. synchronized关键字的诞生
在Java中,synchronized关键字是实现线程同步的基础工具。它的作用是保证同一时间只有一个线程可以执行某个代码块或方法。为了确保线程安全,Java的设计者将wait()和notify()与synchronized紧密地绑定了在一起。
3. 等待与通知的初衷
wait()和notify()最初的设计目的是为了解决多线程之间的协作问题。比如,一个线程可能需要等待某个资源准备就绪后才能继续执行,而另一个线程则负责准备这个资源并通知等待的线程。为了确保这种协作不会引发竞态条件(race condition)或死锁(deadlock),Java的设计者规定了严格的使用规则——wait()和notify()必须在synchronized块中使用。
二、核心原理:为何必须依赖同步块
1. 对象的“监视器”机制
在Java中,每个对象都有一个与之关联的“监视器”(monitor)。当一个线程进入一个synchronized块时,它会自动获取该对象的监视器锁。这个锁的作用是确保同一时间只有一个线程可以执行被同步保护的代码。
2. wait()和notify()的工作机制
-
wait():当一个线程调用wait()方法时,它会释放当前持有的监视器锁,并进入等待状态。只有在接收到其他线程的通知(notify()或notifyAll())后,该线程才会重新竞争锁并恢复执行。
-
notify():notify()的作用是唤醒一个处于等待状态的线程。如果多个线程在等待同一个对象监视器,notify()只会随机唤醒其中一个。
3. 不在同步块中使用wait()/notify()的风险
假设我们不把wait()和notify()放在synchronized块中使用,会发生什么呢?让我们通过代码来模拟一下:
public class UnsafeWait {
public static void main(String[] args) {
Object lock = new Object();
// 线程A:试图等待
Thread threadA = new Thread(() -> {
System.out.println("Thread A is waiting...");
try {
lock.wait(); // 错误:不在同步块中调用wait()
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread A is notified!");
});
// 线程B:试图通知
Thread threadB = new Thread(() -> {
System.out.println("Thread B is notifying...");
lock.notify(); // 错误:不在同步块中调用notify()
System.out.println("Notification sent!");
});
threadA.start();
threadB.start();
}
}
运行这段代码时,你可能会遇到以下问题:
-
IllegalMonitorStateException:当你在没有获取监视器锁的情况下调用wait()或notify(),Java会抛出这个异常。这是因为这些方法只能在持有对象监视器锁的前提下使用。
-
竞态条件(Race Condition):即使没有出现上述异常,线程之间的协作也会变得不可预测。比如,某个线程可能在调用wait()之前就被另一个线程唤醒,导致程序逻辑混乱。
三、实践误区:常见的错误用法
1. 错误的唤醒顺序
有些开发者可能会认为notify()可以随意使用,但实际情况并非如此。例如:
public class NotifyOrderProblem {
private Object lock = new Object();
public void doWork() throws InterruptedException {
synchronized (lock) {
System.out.println("线程进入同步块...");
// 假设这里需要等待某个条件满足
while (!conditionIsMet()) { // 检查条件
lock.wait(); // 等待通知
}
System.out.println("条件满足,继续执行...");
}
}
public void trigger() {
synchronized (lock) {
setConditionMet(true); // 设置条件为已满足
lock.notify(); // 通知等待的线程
}
}
}
在这个例子中,如果notify()和wait()没有正确地在同一个锁对象上使用,就可能导致“假唤醒”(spurious wakeups)问题。虽然这种情况的概率较低,但确实存在。
2. 错误的锁对象
另一个常见的错误是将wait()和notify()与不同的锁对象一起使用。例如:
public class WrongLockObject {
private Object lock1 = new Object();
private Object lock2 = new Object();
public void waitMethod() throws InterruptedException {
synchronized (lock1) { // 锁的是lock1
System.out.println("线程A正在等待...");
lock1.wait(); // 正确,但锁对象是lock1
}
}
public void notifyMethod() {
synchronized (lock2) { // 锁的是lock2
System.out.println("线程B正在通知...");
lock2.notify(); // 正确,但与等待的线程无关
}
}
}
在这个例子中,waitMethod()和notifyMethod()分别锁的是不同的对象。由于它们没有共享同一个监视器锁,线程之间的协作就无法正常进行。
3. 忽视InterruptedException
在使用wait()方法时,如果线程被中断(interrupted),Java会抛出InterruptedException异常。如果开发者不正确地处理这个异常,可能会导致程序崩溃或逻辑错误。
public class InterruptHandling {
private Object lock = new Object();
public void doWork() throws InterruptedException {
synchronized (lock) {
System.out.println("线程进入同步块...");
try {
lock.wait(); // 等待通知,可能被中断
} catch (InterruptedException e) {
// 处理中断异常,比如恢复中断状态或退出循环
Thread.currentThread().interrupt();
throw e;
}
}
}
}
四、正确使用wait()和notify()
1. 确保同步块和锁对象一致
所有调用wait()和notify()的方法必须在同一个锁对象的synchronized块中。例如:
public class CorrectUsage {
private Object lock = new Object();
public void doWork() throws InterruptedException {
synchronized (lock) {
System.out.println("线程A进入同步块...");
while (!conditionIsMet()) { // 检查条件
lock.wait(); // 等待通知
}
System.out.println("条件满足,继续执行...");
}
}
public void trigger() {
synchronized (lock) {
setConditionMet(true); // 设置条件为已满足
lock.notifyAll(); // 唤醒所有等待的线程
}
}
}
2. 使用notifyAll()代替notify()
在某些情况下,使用notify()可能会导致“死锁”(deadlock)或线程饥饿(thread starvation)。因此,建议优先使用notifyAll()来唤醒所有等待的线程。
synchronized (lock) {
setConditionMet(true);
lock.notifyAll(); // 唤醒所有等待的线程
}
3. 处理InterruptedException
在调用wait()方法时,必须正确处理InterruptedException异常。通常的做法是恢复中断状态或退出循环。
synchronized (lock) {
try {
while (!conditionIsMet()) { // 使用while循环检查条件
lock.wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
throw new RuntimeException("线程被中断", e);
}
}
总结
wait()和notify()是Java中实现线程间协作的重要工具,但它们的使用必须严格遵循特定规则。如果不正确地使用这些方法,可能会导致各种并发问题,如IllegalMonitorStateException、竞态条件和死锁等。
为了确保程序的健壮性和正确性,请记住以下几点:
- 始终在synchronized块中调用wait()和notify()。
- 确保所有线程共享同一个锁对象。
- 使用while循环而不是if来检查等待条件。
- 优先使用notifyAll()以避免线程饥饿。
- 正确处理InterruptedException异常。
通过遵循这些最佳实践,开发者可以更安全地在多线程环境中使用wait()和notify()方法。
📚 领取 | 1000+ 套高质量面试题大合集(无套路,闫工带你飞一把)!
成体系的面试题,无论你是大佬还是小白,都需要一套JAVA体系的面试题,我已经上岸了!你也想上岸吗?
闫工精心准备了程序准备面试?想系统提升技术实力?闫工精心整理了 1000+ 套涵盖前端、后端、算法、数据库、操作系统、网络、设计模式等方向的面试真题 + 详细解析,并附赠高频考点总结、简历模板、面经合集等实用资料!
✅ 覆盖大厂高频题型
✅ 按知识点分类,查漏补缺超方便
✅ 持续更新,助你拿下心仪 Offer!
📥 免费领取 👉 点击这里获取资料
已帮助数千位开发者成功上岸,下一个就是你!✨

689

被折叠的 条评论
为什么被折叠?



