线程等待和通知
本篇文章主要介绍线程等待和通知,线程等待(wait)和通知(notify/notifyAll)是多线程协作的核心机制,当多个线程协作时,合理使用wait/notify可避免死锁,确保线程安全。
线程等待和通知
线程等待(wait)和通知(notify/notifyAll)是多线程编程中实现线程间协作的核心机制,简单来说就是让一个线程等待某个条件满足,另一个线程满足条件后通知等待的线程继续执行。
wait() 函数
当线程调用共享对象的wait()方法时,该线程会触发一系列原子性核心行为:
- 立即释放当前持有的该共享对象的监视器锁(Monitor Lock);
- 放弃 CPU 执行权,从 RUNNABLE 状态转为 WAITING 状态,进入该对象专属的等待队列(Wait Set) 挂起;
- 线程会持续挂起,直至满足以下任一条件才会尝试退出等待状态:
(1) 其他线程调用该共享对象的notify()方法(随机唤醒等待队列中1个线程)或notifyAll()方法(唤醒等待队列中所有线程);
(2) 其他线程调用该等待线程的interrupt()方法,此时等待线程会抛出InterruptedException异常并终止等待;
需要注意的是:无论因何种原因唤醒,线程都不会立即恢复执行,必须先重新参与该共享对象监视器锁的竞争,获取锁后才能从wait()方法的暂停处继续执行(notify()/notifyAll() 仅唤醒线程,不释放锁)。
调用前置条件
若调用 wait() 的线程未事先获取该共享对象的监视器锁,JVM 会立即抛出IllegalMonitorStateException运行时异常,如下图所示:

线程获取共享对象监视器锁的合法方式仅有两种:
(1) 执行以该共享对象为锁的synchronized同步代码块:
synchronized (共享变量) {
//doSomething
}
(2) 调用该共享对象的 synchronized 实例方法(锁对象为方法所属的实例本身):
synchronized void add(int a,int b) {
// doSomething
}
调用此方法的线程会自动获取当前实例(this)的监视器锁
核心本质:wait() 操作的是共享对象的等待队列,而等待队列属于对象监视器的一部分,只有持有监视器锁的线程,才能操作该队列。
虚假唤醒
线程调用 wait() 后进入对象的等待队列,理论上应仅在被 notify()/notifyAll() 唤醒且等待条件真实满足时继续执行,但实际场景中,线程可能在无合法唤醒信号或唤醒后等待条件已失效(如其他线程抢先处理了资源)的情况下被唤醒,这种非预期的唤醒行为称为虚假唤醒(Spurious Wakeup)。
虚假唤醒的触发场景
- JVM / 操作系统底层机制:无外部notify()/notifyAll()调用时,操作系统或 JVM 可能因底层调度策略主动唤醒等待线程;
- 中断唤醒:线程在 wait() 期间被 interrupt() 中断唤醒,但等待的业务条件未满足;
- 多线程竞争:多个线程被 notifyAll() 唤醒后,部分线程抢先处理资源,导致后续线程的等待条件失效。
核心解决方案:循环检查等待条件
虚假唤醒的本质是条件不满足时被唤醒,因此必须使用while循环而非if判断等待条件;if仅做一次条件检查,无法应对唤醒后条件失效的场景,while会在线程唤醒后重新校验条件,若条件仍不满足则再次调用wait(),彻底规避虚假唤醒风险。
经典正确范式:
synchronized (obj ){
while (条件不满足) {
obj .wait() ;
}
}
如上代码是经典的调用共享变量wait()方法的实例,首先通过同步块获取obj上面的监视器锁,然后在while循环内调用obj的wait() 方法。
代码示例
示例 1:基础生产者 - 消费者模型(单生产者单消费者)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class CorrespondedThreadTest {
private static BlockingQueue<String> queue = new LinkedBlockingDeque<>();
static int MAX_SIZE = 1;
static class Producer extends Thread {
@Override
public void run() {
//生产线程
synchronized (queue) {
//消费队列满,则等待队列空闲
while (queue.size() == MAX_SIZE) {
try {
//挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//空闲则生成元素,并通知消费者线程
System.out.println("生产");
queue.add("元素");
queue.notify();
}
}
}
static class Consumer extends Thread {
@Override
public void run() {
//生产线程
synchronized (queue) {
//消费队列满,则等待队列空闲
while (queue.size() == 0) {
try {
//挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生产元素放入队列
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
try {
queue.take();
System.out.println("消费");
///消费元素,并通知唤醒生产者线程
queue.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Consumer c = new Consumer();
Producer p = new Producer();
c.start();
p.start();
}
}

上述代码中,queue 作为线程间共享的临界资源,生产者线程调用 queue.wait() 前,已通过 synchronized (queue) 获取该对象的监视器锁,因此调用 wait() 不会抛出 IllegalMonitorStateException 异常。
当生产者线程检测到队列无空闲容量(queue.size() == MAX_SIZE)时,会调用 queue.wait() 使自身进入 WAITING 状态;此处使用 while 循环而非 if 判断条件,核心目的是规避虚假唤醒:即便线程因 JVM 底层调度等非预期原因被唤醒,仍会重新检查队列容量,若队列仍无空闲,线程会再次调用 wait() 挂起,确保只有队列满足生产条件时才执行生产逻辑。
若生产者线程 A 通过 synchronized 获取 queue 的监视器锁后,检测到队列已满并调用 wait(),该线程会主动释放持有的 queue 监视器锁;这一行为是打破死锁的关键:若不释放锁,线程 A 会因队列满挂起且持续持有锁,而其他生产者 / 消费者线程会因无法获取锁阻塞,所有线程均无法推进,满足死锁 “持有并等待” 的必要条件,最终触发死锁。线程 A 释放锁后,其他线程可竞争并获取 queue 的监视器锁进入同步块,从而打破死锁状态。
示例 2:多线程生产者 - 消费者模型(多生产者多消费者)
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class CorrespondedThreadTest1 {
private static BlockingQueue<String> queue = new LinkedBlockingDeque<>();
static int MAX_SIZE = 1;
static class Producer extends Thread {
@Override
public void run() {
while (true){
//生产线程
synchronized (queue) {
//消费队列满,则等待队列空闲
while (queue.size() == MAX_SIZE) {
try {
//挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//空闲则生成元素,并通知消费者线程
queue.add("元素");
System.out.println("生产");
queue.notifyAll();
}
}
}
}
static class Consumer extends Thread {
@Override
public void run() {
while(true){
//生产线程
synchronized (queue) {
//消费队列满,则等待队列空闲
while (queue.size() == 0) {
try {
//挂起当前线程,并释放通过同步块获取的queue.上的锁,让生产者线程可以获取该锁,将生
//产元素放入队列
queue.wait();
} catch (Exception ex) {
ex.printStackTrace();
}
}
try {
queue.take();
System.out.println("消费");
///消费元素,并通知唤醒生产者线程
queue.notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Consumer c = new Consumer();
Producer p = new Producer();
/* Consumer c1 = new Consumer();
Producer p1 = new Producer();*/
Thread.sleep(1000);
c.start();
p.start();
/*c1.start();
p1.start();*/
}
}

public static void main(String[] args) throws InterruptedException {
Consumer c = new Consumer();
Producer p = new Producer();
Consumer c1 = new Consumer();
Producer p1 = new Producer();
Thread.sleep(1000);
c.start();
p.start();
c1.start();
p1.start();
}

生产者线程获取 queue 监视器锁后,若检测到队列已满则调用 wait() 释放锁并挂起,避免死锁;while 循环仍用于规避虚假唤醒。区别在于多线程场景需将 notify() 替换为 notifyAll():notify() 仅随机唤醒等待队列中的一个线程,可能出现生产者唤醒生产者、消费者唤醒消费者的无效唤醒,导致部分线程永久等待;而 notifyAll() 会唤醒所有等待在 queue 上的线程,线程竞争锁后通过 while 循环校验条件,确保只有符合生产 / 消费条件的线程执行核心逻辑,避免线程饥饿。
wait () 被中断唤醒的场景
public class WaitNotifyInterupt {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
try {
System.out.println("---begin---");
//阻塞当前线程
synchronized (obj) {
obj.wait();
}
System.out.println("---end---");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("---begin interrupt threadA---");
threadA.interrupt();
System.out.println("---end interrupt threadA---");
boolean interrupted = threadA.isInterrupted();
System.out.println(interrupted);
}
}

线程调用共享对象的 wait() 方法进入 WAITING 状态后,若其他线程调用该线程的 interrupt() 方法,会触发中断唤醒:threadA 在 obj.wait() 处立即抛出 InterruptedException 异常,终止等待状态并返回;抛出异常后,线程的中断标记会被自动清除(因此示例中 threadA.isInterrupted() 输出 false)。需注意:中断唤醒仅终止线程的等待状态,不会改变共享对象的监视器锁状态,也不保证线程等待的业务条件=成立,因此中断唤醒后仍需通过循环校验条件,避免逻辑错误。
wait (long timeout)
wait(long timeout) 是java.lang.Object类中无参wait()方法的超时重载版本,其核心协作机制(基于对象监视器锁的释放、挂起、唤醒后锁竞争)与无参wait()完全一致,核心差异仅在于新增超时自动唤醒机制。
关键规则如下:
- 超时参数规则:
timeout 参数的单位为毫秒(ms),表示线程最长等待时长;
若 timeout = 0 等价于无参wait()方法,这也是无参wait()的底层实现逻辑(wait()内部直接调用wait(0));
若传递负数的timeout值 JVM 会立即抛出IllegalArgumentException运行时异常(参数合法性校验失败)。 - 返回条件:
条件 1(继承自 wait ()):其他线程调用该共享对象的notify()(随机唤醒 1 个等待线程)或notifyAll()(唤醒所有等待线程);
条件 2(继承自 wait ()):其他线程调用该等待线程的interrupt()方法,线程抛出InterruptedException异常并退出等待;
条件 3(新增超时条件):等待时长超过timeout且未被上述两类条件唤醒,线程主动退出等待状态(无异常抛出)。 - 唤醒后执行逻辑:
线程需先重新参与共享对象监视器锁的竞争,若锁被其他线程持有,会阻塞至获取锁;
获取锁后, 从wait(long timeout) 方法的暂停处继续执行;
超时唤醒仅代表等待时长到期,不代表线程等待的业务条件已满足,因此仍需通过while循环校验条件,规避虚假唤醒风险。 - 异常规则
仅当线程在timeout时长内被interrupt()中断时,才会在wait(long timeout)处抛出InterruptedException;
线程因中断抛出异常后,中断标记会被自动清除,若需保留中断语义,需在 catch 块中手动调用Thread.currentThread().interrupt()恢复标记。
notify() 函数
notify() 是 java.lang.Object 类的原生方法,是基于对象监视器(Monitor)实现的线程间协作机制核心方法之一,其核心作用是唤醒共享对象等待队列中随机一个因调用 wait 系列方法(wait()/wait(long))而进入 WAITING 状态的线程,是实现线程间 “条件满足后通知协作” 的关键手段。
notify () 的唤醒规则:
当线程调用共享对象的notify()方法时,JVM 会从该对象的等待队列中,随机选择一个调用过该对象 wait 系列方法的等待线程进行唤醒;
等待队列中若有多个线程,开发者无法控制具体唤醒哪一个,完全由 JVM 底层调度机制决定;
若该对象的等待队列为空(无线程等待),调用notify()不会产生任何效果,也不会抛出异常。
被 notify() 唤醒的线程的执行逻辑:
状态转换:从 WAITING(等待状态)转为 BLOCKED(阻塞状态),进入该对象的锁竞争队列;
锁竞争:通知线程释放锁后,被唤醒的线程需要与其他正在竞争该锁的线程共同竞争监视器锁;
恢复执行:只有竞争到锁的线程,才能从调用wait()的位置继续执行;若竞争失败,会持续处于 BLOCKED 状态,直到再次抢到锁。
调用前置条件
调用前置条件与 wait 系列方法完全一致,notify() 调用线程必须先获取该共享对象的监视器锁(内置锁),否则 JVM 会立即抛出 IllegalMonitorStateException 运行时异常。

合法调用场景仅两种(与 wait () 一致):
(1) 在以该共享对象为锁的synchronized同步代码块内调用:
synchronized (共享对象) {
共享对象.notify(); // 合法:当前线程持有共享对象的锁
}
(2) 在该共享对象的synchronized实例方法内调用(锁为this):
synchronized void notifyThread() {
this.notify(); // 等价于notify(),合法
}
核心本质:notify()操作的是共享对象的等待队列,而等待队列属于对象监视器的一部分,只有持有监视器锁的线程,才能操作该队列。
需要注意的是:调用 notify() 的线程,在执行 notify() 方法后,不会立即释放所持有的共享对象监视器锁;监视器锁仍由调用 notify() 的线程持有,直到调用 notify() 的线程退出 synchronized 同步代码块 / 同步方法,锁才会被释放;
示例逻辑:
synchronized (queue) {
queue.notify(); // 调用notify(),但锁仍由当前线程持有
System.out.println("执行notify后,锁仍未释放"); // 这行代码执行完,退出同步块,锁才释放
}
代码示例
public class Basket {
private boolean isEmpty;
public boolean isEmpty() {
return isEmpty;
}
public void setEmpty(boolean empty) {
isEmpty = empty;
}
}
public class Producer extends Thread {
private Basket basket;
public Producer(Basket basket){
super();
this.basket = basket;
}
@Override
public void run() {
while(true){
//定义一个同步代码块
synchronized (basket){
try {
if(!basket.isEmpty()){
//线程等待的状态
basket.wait();
}
System.out.println("生成水果");
basket.setEmpty(false);
//通知在这个共享对象上等待的线程
basket.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
public class Consumer extends Thread{
private Basket basket;
public Consumer(Basket basket){
super();
this.basket = basket;
}
@Override
public void run() {
while(true){
//定义一个同步代码块
synchronized (basket){
try {
if(basket.isEmpty()){
//线程等待的状态
basket.wait();
}
System.out.println("消费水果");
basket.setEmpty(true);
//通知在这个共享对象上等待的线程
basket.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Test {
public static void main(String[] args) {
Basket basket = new Basket();
Producer producer = new Producer(basket);
Consumer consumer = new Consumer(basket);
Thread t = new Thread(producer);
Thread t1 = new Thread(consumer);
t.start();
t1.start();
}
}

如上代码的单生产者 - 单消费者场景下 notify() 可安全使用;
但在多生产者 - 多消费者场景下,若使用 notify(),可能出现无效唤醒:
生产者线程调用notify(),随机唤醒了另一个生产者线程(而非消费者线程);被唤醒的生产者线程竞争到锁后,检查队列仍满,会再次调用 wait() 挂起,而真正需要被唤醒的消费者线程永远无法被唤醒,导致线程饥饿;优先使用 notifyAll(),结合while循环校验条件,避免无效唤醒和线程饥饿。
notifyAll()
notifyAll() 是 java.lang.Object 类的原生方法,是基于对象监视器(Monitor)实现的线程协作核心方法,其核心作用是:唤醒共享对象等待队列中所有因调用 wait 系列方法(wait()/wait(long))而进入 WAITING 状态的线程,是解决多线程场景下notify() 随机唤醒导致线程饥饿问题的核心方案。
唤醒规则:
调用 notifyAll() 后,JVM 会将该共享对象等待队列中的所有等待线程全部移出。
若共享对象的等待队列为空,调用notifyAll()不会产生任何效果,也不会抛出异常。
被唤醒线程的执行逻辑:
所有被唤醒的线程从WAITING状态转为BLOCKED状态,进入该对象的锁竞争队列;
调用notifyAll()的线程退出同步块、释放监视器锁后,这些被唤醒的线程会与其他竞争该锁的线程共同竞争锁;
只有抢到锁的线程,才能从wait()方法的暂停处继续执行;执行完毕释放锁后,下一个抢到锁的线程再执行,直至所有被唤醒线程全部完成执行。
调用前置条件
只有持有该共享对象监视器锁的线程才能调用,否则 JVM 会抛出IllegalMonitorStateException运行时异常;且调用 notifyAll() 时不会立即释放锁,锁需等调用线程退出synchronized同步块 / 方法后才释放。
不同于在共享变量上调用notify()函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll() 方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
代码示例
public class NotifyAllTest {
//创建资源
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
//创建线程
Thread threadA = new Thread(new Runnable() {
public void run() {
//获取resourceA共享资源的监视器锁
synchronized (resourceA) {
System.out.println("threadA get resourceA lock");
try {
System.out.println("threadA begin wait");
resourceA.wait();
System.out.println("threadA end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
//创建线程
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println("threadB get resourceA lock");
try {
System.out.println("threadB begin wait");
resourceA.wait();
System.out.println("threadB end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
//创建线程
Thread threadC = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println("threadC begin notify");
resourceA.notify();
//resourceA.notifyAll();
}
}
});
//启动线程
threadA.start();
threadB.start() ;
Thread. sleep (1000) ;
threadC.start() ;
//等待线程结束
threadA. join() ;
threadB. join() ;
threadC.join() ;
System.out.println ("main over") ;
}
}
如上代码开启了三个线程,其中线程A和线程B分别调用了共享资源resourceA的wait()方法,线程C则调用了nofity() 方法。这里启动线程C前首先调用sleep 方法让主线程休眠1s,这样做的目的是让线程A和线程B全部执行到调用wait方法后再调用线程C的notify方法。这个例子试图在线程A和线程B都因调用共享资源resourceA的wait()方法而被阻塞后,让线程C再调用resourceA的notify()方法,从而唤醒线程A和线程B。但是从执行结果来看,只有一个线程A被唤醒,线程B没有被唤醒。

从输出结果可知线程调度器这次先调度了线程A占用CPU来运行,线程A首先获取resourceA上面的锁,然后调用resourceA的wait()方法挂起当前线程并释放获取到的锁,然后线程B获取到resourceA上的锁并调用resourceA的wait()方法,此时线程B也被阻塞挂起并释放了resourceA. 上的锁,到这里线程A和线程B都被放到了resourceA 的阻塞集合里面。线程C休眠结束后在共享资源resourceA上调用了notify() 方法,这会激活resourceA的阻塞集合里面的一个线程,这里激活了线程A,所以线程A调用的wait()方法返回了,线程A执行完毕。而线程B还处于阻塞状态。如果把线程C调用的notify()方法改为调用notifyAll()方法,则执行结果如下。

从输入结果可知线程A和线程B被挂起后,线程C调用notifyAll()方法会唤醒resourceA的等待集合里面的所有线程,这里线程A和线程B都会被唤醒,只是线程B先获取到resourceA.上的锁,然后从wait()方法返回。线程B执行完毕后,线程A又获取了resourceA上的锁,然后从wait()方法返回。线程A执行完毕后,主线程返回,然后打印输出。
LockSupport
LockSupport 是 java.util.concurrent.locks 包下的基础工具类,JDK 官方定义为创建锁和其他同步类的线程阻塞原语—— 它是 JUC 中几乎所有同步组件的底层实现基石,核心作用是通过 “许可” 机制实现线程的精准阻塞与唤醒,功能上替代了传统的 wait/notify,灵活性和易用性远超后者。
wait()、notify()(含notifyAll())是基于 Object 监视器实现的传统线程协作方法,核心缺点如下:
- 强锁依赖
wait/notify 操作的是对象监视器的等待队列,只有持有监视器锁的线程才拥有操作该队列的权限;
该限制大幅压缩了方法的使用场景,无法在无锁的业务逻辑中实现线程阻塞 / 唤醒,灵活性极低。 - 唤醒粒度不可控
当共享对象的等待队列中有多个线程时,notify() JVM 会随机选择一个等待线程唤醒,开发者无法干预唤醒目标,若唤醒的是无需执行的线程,会导致无效唤醒,甚至引发线程饥饿;
notifyAll() 虽能唤醒所有等待线程,但所有被唤醒线程会竞争同一把监视器锁,仅能串行执行,且仍无法定向唤醒特定线程;

- LockSupport.park() 是 LockSupport 的核心阻塞方法,其核心功能是:阻塞当前执行线程,直至该线程获得许可、被中断或 JVM 内部原因唤醒。
- park(Object blocker) 是 park() 的增强版本,核心功能与 park() 完全一致,唯一区别是新增 blocker 参数(阻塞关联对象),该参数的核心价值是辅助问题排查与监控。
- LockSupport.unpark(Thread thread) 是唤醒线程的核心方法,其核心功能是:为指定线程发放许可(Permit),若该线程正处于 park 阻塞状态,则立即唤醒;若未阻塞,许可会被保留。
代码示例:
import java.util.concurrent.locks.LockSupport;
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
Thread parkThread = new Thread(new ParkThread());
parkThread.start();
Thread.sleep(3000);
LockSupport.unpark(parkThread);
}
static class ParkThread implements Runnable{
@Override
public void run() {
System.out.println("开始线程阻塞");
//挂起当前的线程
LockSupport.park();
System.out.println("结束线程阻塞");
}
}
}

更多推荐


所有评论(0)