Java--多线程--线程安全3
上篇我们深入了解了一下 volatile 关键字,本篇我们来讲解 wait 和 notify 关键字~
1. 为什么需要 wait 和 notify?
在多线程编程中,线程之间经常需要协作。比如:
-
一个线程生产数据,另一个线程消费数据。当数据没准备好时,消费者需要等待;当数据准备好时,生产者需要通知消费者。
-
多个线程共同完成一项任务,需要互相协调步骤。
Java 的 wait() 和 notify() 就是用来解决这类线程间通信问题的基本机制。它们允许一个线程在某个条件不满足时主动进入等待状态(释放CPU和锁),直到另一个线程改变条件并唤醒它。
2. 基础概念
2.1 对象锁(监视器锁,Monitor)
-
每个 Java 对象都有一个内置锁,也叫监视器锁。
-
当线程进入
synchronized代码块或方法时,会自动获得该对象的锁;退出时释放锁。 -
同一时刻,只有一个线程能持有某个对象的锁。
2.2 等待集(Wait Set)
-
每个对象除了锁,还关联着一个等待集。
-
当线程调用对象的
wait()方法时,它会释放该对象的锁,并进入该对象的等待集,状态变为WAITING。 -
其他线程调用该对象的
notify()或notifyAll()时,会从等待集中唤醒一个或所有线程。
3. wait 方法详解
wait() 是 Object 类的方法,有三个重载版本:
public final void wait() throws InterruptedException
//使当前线程无限期等待,直到另一个线程调用此对象的 notify() 或 notifyAll() 方法将其唤醒。
public final void wait(long timeout) throws InterruptedException
//使当前线程等待指定的毫秒数。如果在 timeout 毫秒内被唤醒,则提前结束等待;如果超时,则自动唤醒。
public final void wait(long timeout, int nanos) throws InterruptedException
//提供更精确的超时控制,等待 timeout 毫秒加上 nanos 纳秒。
3.1 wait() 做了什么?
-
前提:当前线程必须持有该对象的锁(即在
synchronized块内)。 -
调用
wait()后:-
当前线程释放该对象的锁。
-
线程进入该对象的等待集,状态变为
WAITING。 -
线程暂停执行,直到以下情况发生:
-
其他线程调用该对象的
notify()或notifyAll()将其唤醒。 -
其他线程中断该线程(抛出
InterruptedException)。 -
(如果使用了超时版本)等待时间超时。
-
-
3.2 被唤醒后
-
线程从等待集中移除,重新成为该对象锁的竞争者。
-
当它再次获得锁后,才会从
wait()调用处继续往后执行。 -
注意:被唤醒的线程不会立即执行,必须等待唤醒它的线程释放锁,然后它和其他线程竞争锁成功后才能执行。
混淆点:很多人以为 wait() 会使线程一直等待直到被唤醒,但忽略了它必须重新竞争锁才能继续执行。被唤醒并不意味着立即执行。
易错点:在调用 wait() 之前必须持有锁,否则抛出 IllegalMonitorStateException。
面试考点:wait() 会释放锁吗?释放的是哪把锁?——会释放当前对象锁,但不会释放其他对象的锁(如果有嵌套同步)。
4. notify 和 notifyAll 详解
4.1 notify()
-
前提:当前线程必须持有该对象的锁。
-
作用:从该对象的等待集中随机唤醒一个线程(具体由 JVM 实现决定,不可控)。
-
被唤醒的线程会进入锁的竞争队列(入口集),等待获得锁。
4.2 notifyAll()
-
前提:当前线程必须持有该对象的锁。
-
作用:唤醒该对象等待集中的所有线程。
-
所有被唤醒的线程都会进入竞争队列,一起竞争锁。
4.3 重要说明
-
notify()和notifyAll()并不会释放锁,只是唤醒其他线程。锁的释放需要等到同步块或方法执行完毕。 -
如果唤醒的线程发现条件仍不满足,它可能会再次调用
wait()重新进入等待。
混淆点:notify() 后,被唤醒的线程是否立即获得锁?——不是,它需要等待当前线程释放锁,然后和其他线程竞争。
易错点:如果使用 notify() 但唤醒的线程不满足条件,它可能再次等待,而其他符合条件的线程未被唤醒,可能导致死锁。
面试考点:notify() 和 notifyAll() 的区别,什么时候用哪个?
5. 为什么 wait 必须放在循环中?
5.1 虚假唤醒(Spurious Wakeup)
-
在某些操作系统或 JVM 实现中,即使没有调用
notify或notifyAll,等待的线程也可能被意外唤醒。 -
这种现象称为虚假唤醒,是底层系统的行为,无法完全避免。
5.2 正确的做法
因此,wait() 必须放在一个 while 循环中,循环检查条件,而不是用 if 判断一次。这样可以保证即使被虚假唤醒,也会重新检查条件,如果条件不满足就继续等待。
synchronized (lock) {
while (条件不满足) { // 必须用 while
lock.wait();
}
// 条件满足,执行后续操作
}
如果用 if,线程被唤醒后就直接往下执行,如果条件实际上并不满足(虚假唤醒),就会导致逻辑错误。
易错点:新手常犯错误是用 if 判断条件,忽略了虚假唤醒。
面试考点:什么是虚假唤醒?为什么 wait 要放在循环中?——考察对线程安全细节的理解。
6. 基础代码示例(逐步深入)
6.1 示例1:最简单的 wait/notify
public class SimpleWaitNotify {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waiter = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("[" + Thread.currentThread().getName() + "] 获得锁,即将 wait...");
lock.wait(); // ①
System.out.println("[" + Thread.currentThread().getName() + "] 被唤醒,重新获得锁,继续执行");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Waiter");
Thread notifier = new Thread(() -> {
synchronized (lock) {
System.out.println("[" + Thread.currentThread().getName() + "] 获得锁,即将 notify...");
lock.notify(); // ②
System.out.println("[" + Thread.currentThread().getName() + "] 已 notify,但还持有锁,继续工作");
try {
Thread.sleep(1000); // ③ 模拟持有锁做事情
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("[" + Thread.currentThread().getName() + "] 释放锁");
}
}, "Notifier");
waiter.start();
Thread.sleep(100); // 确保 waiter 先启动
notifier.start();
}
}
代码详解:
-
waiter 线程启动:首先进入
synchronized(lock),成功获得锁,打印消息。然后调用lock.wait(),此时:-
释放
lock的锁。 -
进入
lock的等待集,状态WAITING。 -
暂停执行。
-
-
主线程 sleep(100):确保 waiter 先进入等待,避免 notify 信号丢失。
-
notifier 线程启动:因为 waiter 已释放锁,notifier 获得锁,打印消息,调用
lock.notify():-
从等待集中随机选择一个线程(waiter)唤醒。
-
被唤醒的 waiter 移动到入口集,但尚未获得锁,不能执行。
-
notifier 继续持有锁,执行 sleep(1000),期间 waiter 无法获得锁。
-
sleep 结束后,notifier 退出同步块,释放锁。
-
-
waiter 重新获得锁:从
wait()之后继续执行,打印被唤醒消息。
输出示例(顺序可能略有不同,但逻辑一致):
[Waiter] 获得锁,即将 wait... [Notifier] 获得锁,即将 notify... [Notifier] 已 notify,但还持有锁,继续工作 [Notifier] 释放锁 [Waiter] 被唤醒,重新获得锁,继续执行
面试考点:分析上述代码的执行顺序,特别是被唤醒线程为什么不能立即执行。
6.2 示例2:while 循环的必要性(模拟虚假唤醒)
public class WaitInLoop {
private static final Object lock = new Object();
private static boolean condition = false;
public static void main(String[] args) throws InterruptedException {
Thread waiter = new Thread(() -> {
synchronized (lock) {
while (!condition) { // 使用 while 循环检查
try {
System.out.println("条件不满足,进入等待...");
lock.wait();
System.out.println("被唤醒,但继续检查条件...");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("条件满足,开始工作!");
}
});
waiter.start();
Thread.sleep(500);
synchronized (lock) {
condition = true;
lock.notify();
}
}
}
代码详解:
-
waiter 线程启动后,发现
condition为 false,进入while循环,调用wait()释放锁并等待。 -
主线程 sleep 500ms 后,获得锁,修改
condition = true,然后notify()唤醒 waiter。 -
waiter 被唤醒后,重新获得锁,从
wait()返回,进入下一次循环检查condition,此时为 true,退出循环,执行后续工作。
假如用 if 会发生什么?
如果改用 if (!condition) { wait(); },线程被唤醒后会直接执行“条件满足,开始工作!”的代码。但如果发生虚假唤醒(即使没有 notify,线程也可能被唤醒),此时 condition 可能仍为 false,程序就会错误地认为条件满足,导致逻辑错误。
易错点:未使用 while 循环检查条件。
面试考点:解释虚假唤醒,以及为什么必须用 while。
6.3 示例3:notifyAll 唤醒所有等待线程
public class NotifyAllDemo {
private static final Object lock = new Object();
private static boolean ready = false;
public static void main(String[] args) throws InterruptedException {
Runnable waiterTask = () -> {
synchronized (lock) {
while (!ready) {
try {
System.out.println(Thread.currentThread().getName() + " 等待...");
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println(Thread.currentThread().getName() + " 继续执行");
}
};
for (int i = 1; i <= 3; i++) {
new Thread(waiterTask, "Waiter-" + i).start();
}
Thread.sleep(1000); // 确保所有等待线程都已启动
new Thread(() -> {
synchronized (lock) {
ready = true;
// lock.notify(); // ① 只唤醒一个
lock.notifyAll(); // ② 唤醒所有
System.out.println("Notifier 唤醒了所有等待线程");
}
}, "Notifier").start();
}
}
代码详解:
-
三个 waiter 线程依次启动,每个都尝试获得锁。但只有一个能先获得锁(假设 Waiter-1),其他两个会阻塞在同步块入口。
-
Waiter-1 获得锁后,发现
ready为 false,调用wait(),释放锁,进入等待集。 -
此时锁可用,Waiter-2 获得锁,同样发现条件不满足,调用
wait(),释放锁,进入等待集。 -
Waiter-3 获得锁,同样调用
wait(),进入等待集。至此三个线程都在等待集中。 -
Notifier 线程启动,获得锁,将
ready设为 true,然后调用notifyAll()。-
所有三个 waiter 线程被唤醒,从等待集移动到入口集。
-
Notifier 线程继续持有锁,直到退出同步块释放锁。
-
-
锁释放后,三个 waiter 线程竞争锁,只有一个能先获得锁(假设 Waiter-2),执行后续工作(打印“继续执行”),然后释放锁。接下来另外两个依次获得锁并执行。
如果使用 notify() 会怎样?
-
只会唤醒一个线程(例如 Waiter-1),其余两个仍留在等待集,永远无法被唤醒,除非后续还有 notify。
面试考点:notify() 可能引起死锁的情况(被唤醒线程条件不满足且没有其他线程唤醒)。
6.4 示例4:生产者-消费者模型(经典案例)
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumer {
private static final int CAPACITY = 5;
private final Queue<Integer> buffer = new LinkedList<>();
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(pc.new Producer(), "Producer");
Thread consumer = new Thread(pc.new Consumer(), "Consumer");
producer.start();
consumer.start();
}
class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
synchronized (buffer) {
while (buffer.size() == CAPACITY) {
try {
System.out.println("缓冲区满,生产者等待...");
buffer.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("生产者生产:" + value);
buffer.offer(value++);
buffer.notifyAll(); // 唤醒可能等待的消费者
}
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (buffer) {
while (buffer.isEmpty()) {
try {
System.out.println("缓冲区空,消费者等待...");
buffer.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Integer value = buffer.poll();
System.out.println("消费者消费:" + value);
buffer.notifyAll();
}
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
}
代码详解:
-
生产者和消费者共享同一个
buffer队列,同步块锁住buffer。 -
生产者:
-
获得锁后,检查缓冲区是否已满(
buffer.size() == CAPACITY)。如果满,进入while循环调用buffer.wait(),释放锁并等待。 -
被唤醒后(通常是消费者消费后调用了
notifyAll),重新检查条件。如果不满,生产一个数据,放入队列,然后调用buffer.notifyAll()唤醒可能等待的消费者。 -
生产后释放锁(退出同步块),然后 sleep 模拟生产间隔。
-
-
消费者:
-
获得锁后,检查缓冲区是否为空。如果空,调用
wait()等待。 -
被唤醒后(通常是生产者生产后调用了
notifyAll),重新检查条件。如果不空,取出数据消费,然后调用notifyAll()唤醒可能等待的生产者。 -
消费后释放锁,sleep 模拟消费间隔。
-
关键点:
-
使用
while循环检查条件,防止虚假唤醒。 -
使用
notifyAll而不是notify,因为可能有多生产者多消费者,确保安全。 -
注意生产者和消费者的等待条件不同,但使用同一个锁对象
buffer。
易错点:
-
忘记在条件检查中使用
while。 -
在单生产者-单消费者场景下,可以使用
notify提高效率,但需要确保不会发生信号丢失。 -
在同步块内调用
wait()后,其他线程可以进入同步块,因为锁已释放。
面试考点:手写生产者-消费者代码,并解释为什么用 while 和 notifyAll。
7. 常见错误与注意事项
7.1 不在同步块中调用 wait/notify
Object lock = new Object();
lock.wait(); // 抛出 IllegalMonitorStateException
必须在 synchronized(lock){...} 内调用。
7.2 信号丢失
如果线程在调用 wait() 之前,其他线程已经调用了 notify(),那么 notify 信号就会丢失,导致等待线程永远等下去。因此要确保等待线程先进入等待状态,或者使用更高级的工具(如 CountDownLatch)来协调。
7.3 notify 与 notifyAll 的选择
-
notify:只唤醒一个线程,适用于所有等待线程条件相同且只需一个线程处理的情况(如单生产者-单消费者)。但如果被唤醒的线程条件仍不满足,它可能再次等待,而其他符合条件的线程却没有被唤醒,导致死锁。
-
notifyAll:唤醒所有线程,更安全,但可能会引起不必要的竞争。在大多数示例中,使用
notifyAll是稳妥的选择。
7.4 中断处理
wait() 会抛出 InterruptedException,需要在 catch 块中正确处理(通常设置中断标志或退出循环)。
8. wait 与 sleep 的区别
| 比较点 | wait | sleep |
|---|---|---|
| 所属类 | Object | Thread |
| 是否释放锁 | 是,释放当前对象的锁 | 否,不释放锁 |
| 唤醒方式 | 需要 notify/notifyAll 或超时 | 时间到自动唤醒,或可被中断 |
| 使用场景 | 线程间协作,等待条件满足 | 暂停当前线程一段时间 |
| 同步要求 | 必须在同步块/方法中调用 | 无同步要求 |
| 方法性质 | 实例方法 | 静态方法 |
面试考点:常问区别,以及为什么 wait 需要同步而 sleep 不需要。
8.1 wait 的语义要求必须持有锁
wait() 的语义是:当前线程释放对象的锁,并进入等待状态。既然要释放锁,前提就是当前线程必须已经持有该对象的锁。如果线程没有持有锁就调用 wait(),它无法释放不存在的锁,因此 JVM 会抛出 IllegalMonitorStateException。
8.2 防止竞态条件(信号丢失)
wait 通常与条件变量一起使用,例如:
synchronized (lock) {
while (!condition) {
lock.wait();
}
// 条件满足,继续执行
}
这里有两个关键操作:检查条件 和 进入等待。如果这两个操作不放在同一个同步块中,就可能发生以下竞态条件:
-
线程 A 检查条件,发现不满足(
condition为 false),准备调用wait()。 -
此时线程 B 获得锁,将
condition设为 true,并调用notify()。 -
线程 A 随后才调用
wait(),但由于线程 B 的notify已经执行,信号丢失,线程 A 可能永远等待。
将条件检查和 wait 放在同一个同步块中,保证了这两个操作的原子性(因为线程 A 持有锁,线程 B 无法在中间插入修改条件和 notify)。所以 wait 必须在同步块中调用。
8.3 sleep 不需要同步的原因
sleep 只是让当前线程暂停执行,不涉及任何共享资源的操作,也不释放锁。它纯粹是线程自身的行为,与其他线程无关。因此,不需要同步来保护任何共享变量或避免竞态条件。在任何地方调用 sleep 都不会破坏线程安全性。
深入讲解:
-
为什么
wait必须释放锁:因为wait的语义是让出 CPU 并等待条件变化,如果不释放锁,其他线程无法进入同步块修改条件,就会导致死锁。 -
sleep不释放锁:sleep只是让线程暂停执行,并不涉及锁的释放,所以如果在同步块中调用sleep,其他线程仍然无法获得锁。 -
面试高频问题:
wait(1000)和sleep(1000)的区别?wait(1000)会释放锁,且可以被notify提前唤醒;sleep(1000)不释放锁,且只能通过中断唤醒。
9. 混淆点、易错点与面试考点总结
混淆点
-
wait()释放的是哪把锁? —— 只释放调用wait()的对象的锁,如果有嵌套同步,其他对象的锁仍持有。 -
notify()后,被唤醒的线程何时执行? —— 需要等待当前线程释放锁,并且被唤醒线程竞争到锁后才能执行。 -
notifyAll()唤醒所有线程,它们是否同时执行? —— 不是,它们需要依次竞争锁,串行执行同步块内的代码。 -
虚假唤醒是 JVM bug 吗? —— 不是,是某些操作系统层面的行为,JVM 规范允许,因此必须处理。
易错点
-
没有在同步块中调用
wait/notify,导致IllegalMonitorStateException。 -
使用
if而不是while检查条件,导致虚假唤醒时的逻辑错误。 -
信号丢失:在等待线程执行
wait()之前,其他线程已经调用了notify。 -
死锁:使用
notify但唤醒的线程不满足条件,又没有其他线程唤醒。 -
忽略中断:没有正确处理
InterruptedException,导致线程无法响应中断。 -
在
wait()之后修改了共享变量但没有重新检查条件(如果不用 while,就会有问题)。
面试考点
-
手写生产者-消费者模型(经典题)。
-
wait和sleep的区别(高频题)。 -
为什么
wait必须在同步块中? —— 为了保证线程安全,避免丢失更新和信号丢失。 -
虚假唤醒是什么?如何避免? —— 用 while 循环检查条件。
-
notify和notifyAll的区别及应用场景。 -
线程状态变化:调用
wait后线程进入什么状态?被唤醒后进入什么状态? -
锁的释放:
wait释放锁,sleep不释放锁。 -
死锁分析:给出一个使用
wait/notify的代码,判断是否可能死锁。
解答
第一题上面有相关代码,这里不赘述
2. wait 和 sleep 的区别
| 比较点 | wait | sleep |
|---|---|---|
| 所属类 | Object 的实例方法 |
Thread 的静态方法 |
| 是否释放锁 | 是,释放当前对象的锁 | 否,不释放任何锁 |
| 唤醒方式 | 需要其他线程调用 notify/notifyAll 或超时 |
时间到自动唤醒,或可被中断 |
| 使用场景 | 线程间通信,等待某个条件成立 | 暂停当前线程执行一段时间 |
| 同步要求 | 必须在 synchronized 块或方法中调用 |
无要求 |
| 方法性质 | 依赖于对象监视器 | 线程级别的暂停 |
深入讲解:
-
为什么
wait必须释放锁:因为wait的语义是让出 CPU 并等待条件变化,如果不释放锁,其他线程无法进入同步块修改条件,就会导致死锁。 -
sleep不释放锁:sleep只是让线程暂停执行,并不涉及锁的释放,所以如果在同步块中调用sleep,其他线程仍然无法获得锁。 -
面试高频问题:
wait(1000)和sleep(1000)的区别?wait(1000)会释放锁,且可以被notify提前唤醒;sleep(1000)不释放锁,且只能通过中断唤醒。
3. 为什么 wait 必须在同步块中?
核心原因:为了保证条件检查和等待操作的原子性,避免发生竞态条件。
反例分析:
// 错误的写法(非同步)
if (!condition) {
lock.wait(); // 不在同步块中,会抛 IllegalMonitorStateException
}
即使我们假设可以这样写,假设线程 A 检查到 condition 为 false,准备调用 wait(),但此时线程 B 抢先把 condition 设为 true 并调用了 notify(),然后线程 A 才调用 wait(),那么线程 A 就会永远等待下去(信号丢失)。
正确的做法:
synchronized (lock) {
while (!condition) {
lock.wait();
}
}
在同步块中,检查条件和调用 wait 是原子的(因为持有锁),其他线程无法在中间修改条件,从而保证了正确性。
补充:wait 内部会释放锁,所以进入等待集后,其他线程可以获得锁并修改条件。
4. 虚假唤醒是什么?如何避免?
定义:虚假唤醒是指一个线程在没有收到 notify/notifyAll 的情况下,从 wait 状态中被唤醒。这是某些操作系统底层的特性,Java 规范允许这种现象发生。
为什么会有虚假唤醒:为了提高实现效率,一些操作系统的等待/通知机制可能允许线程在特定条件下(如信号中断)被唤醒,但 Java 无法区分这些情况,因此规定 wait 可以无条件返回。
如何避免:必须将 wait 放在一个 while 循环中,循环检查条件。这样即使发生虚假唤醒,线程会重新检查条件,如果条件不满足,则继续等待。
代码示例:
synchronized (lock) {
while (!condition) { // 用 while,不是 if
lock.wait();
}
// 条件满足,继续执行
}
如果不处理虚假唤醒会怎样?-- 可能会导致线程在条件不满足时继续执行,破坏程序的不变性,引发数据不一致甚至崩溃。
5. notify 和 notifyAll 的区别及应用场景
| 特性 | notify | notifyAll |
|---|---|---|
| 唤醒数量 | 随机唤醒等待集中的一个线程 | 唤醒等待集中的所有线程 |
| 锁竞争 | 只有一个线程会竞争锁 | 所有被唤醒的线程都会竞争锁 |
| 安全性 | 较低,可能发生信号丢失或死锁 | 较高,所有等待线程都有机会执行 |
| 性能 | 较高(减少上下文切换) | 较低(可能引起“惊群效应”) |
选择原则:
-
当所有等待线程条件相同且只需一个线程处理时,可以使用
notify(例如单生产者-单消费者模型)。但要确保被唤醒的线程一定能处理任务,否则可能导致死锁。 -
当等待线程条件不同,或者存在多个生产者/消费者时,必须使用
notifyAll,否则可能发生信号丢失。
死锁示例(使用 notify 导致):
// 假设有两个等待线程:生产者(等待空间)和消费者(等待数据) // 如果使用 notify,可能唤醒一个生产者,但生产者检查到缓冲区满,继续等待 // 而消费者没有被唤醒,永远无法消费,导致死锁。
6. 线程状态变化
调用 wait() 前后的状态变化:
-
调用前:线程处于
RUNNABLE状态(或BLOCKED如果正在竞争锁,但一旦获得锁,就是RUNNABLE)。 -
调用
wait()后:线程释放锁,进入该对象的等待集,状态变为WAITING(或TIMED_WAITING如果使用超时版本)。 -
被
notify唤醒后:线程从等待集移到入口集,状态变为BLOCKED(等待锁),直到获得锁后才变为RUNNABLE。 -
获得锁后:从
wait()返回,继续执行。
图解:
RUNNABLE --> 获得锁 --> 调用 wait() --> WAITING
^ |
| | 被 notify
| v
+------ 获得锁 <---- BLOCKED <--+
7. 锁的释放
-
wait():释放当前对象的锁(仅释放调用wait的那个对象的锁)。如果线程持有多个对象的锁(嵌套同步),其他锁不会释放。 -
sleep()/yield():不释放任何锁。 -
线程终止:自动释放所有持有的锁。
重要概念:wait 释放锁后,线程进入等待集,其他线程可以获取该对象的锁。当线程被唤醒并重新获得锁后,它才继续执行。
8. 死锁分析
题目:给出一个使用 wait/notify 的代码,判断是否可能死锁。
示例:
// 线程 A
synchronized (lockA) {
lockA.wait(); // ①
synchronized (lockB) {
// ...
}
}
// 线程 B
synchronized (lockB) {
lockB.wait(); // ②
synchronized (lockA) {
// ...
}
}
这个代码不会死锁,因为 wait 会释放锁,所以线程 A 在①处释放 lockA,线程 B 在②处释放 lockB,它们可以互相进入对方的同步块。但如果 wait 换成 sleep,就会死锁。
典型死锁场景(使用 notify 错误):
-
多个线程等待不同条件,使用
notify唤醒一个,但被唤醒的线程条件不满足,再次等待,导致其他线程永远没机会被唤醒。
死锁条件:
-
互斥
-
持有并等待
-
不可剥夺
-
循环等待
wait/notify 可以打破“不可剥夺”条件(因为 wait 释放锁),但如果不正确使用,仍可能因信号丢失导致逻辑上的死锁(线程永远等待)。
总结
以上是对 wait/notify 常见面试考点的详细讲解。在面试中,不仅要能写出代码,还要能解释背后的原理和细节,特别是:
-
为什么用
while -
为什么用
notifyAll -
虚假唤醒
-
锁的释放
-
与
sleep的区别
好啦~以上就是本篇的全部内容啦~ 全是干货~~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)