上篇我们深入了解了一下 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() 后:

    1. 当前线程释放该对象的锁。

    2. 线程进入该对象的等待集,状态变为 WAITING

    3. 线程暂停执行,直到以下情况发生:

      • 其他线程调用该对象的 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();
    }
}

代码详解

  1. waiter 线程启动:首先进入 synchronized(lock),成功获得锁,打印消息。然后调用 lock.wait(),此时:

    • 释放 lock 的锁。

    • 进入 lock 的等待集,状态 WAITING

    • 暂停执行。

  2. 主线程 sleep(100):确保 waiter 先进入等待,避免 notify 信号丢失。

  3. notifier 线程启动:因为 waiter 已释放锁,notifier 获得锁,打印消息,调用 lock.notify()

    • 从等待集中随机选择一个线程(waiter)唤醒。

    • 被唤醒的 waiter 移动到入口集,但尚未获得锁,不能执行。

    • notifier 继续持有锁,执行 sleep(1000),期间 waiter 无法获得锁。

    • sleep 结束后,notifier 退出同步块,释放锁。

  4. 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();
    }
    // 条件满足,继续执行
}

这里有两个关键操作:检查条件 和 进入等待。如果这两个操作不放在同一个同步块中,就可能发生以下竞态条件:

  1. 线程 A 检查条件,发现不满足(condition 为 false),准备调用 wait()

  2. 此时线程 B 获得锁,将 condition 设为 true,并调用 notify()

  3. 线程 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. 混淆点、易错点与面试考点总结

混淆点

  1. wait() 释放的是哪把锁? —— 只释放调用 wait() 的对象的锁,如果有嵌套同步,其他对象的锁仍持有。

  2. notify() 后,被唤醒的线程何时执行? —— 需要等待当前线程释放锁,并且被唤醒线程竞争到锁后才能执行。

  3. notifyAll() 唤醒所有线程,它们是否同时执行? —— 不是,它们需要依次竞争锁,串行执行同步块内的代码。

  4. 虚假唤醒是 JVM bug 吗? —— 不是,是某些操作系统层面的行为,JVM 规范允许,因此必须处理。

易错点

  1. 没有在同步块中调用 wait/notify,导致 IllegalMonitorStateException

  2. 使用 if 而不是 while 检查条件,导致虚假唤醒时的逻辑错误。

  3. 信号丢失:在等待线程执行 wait() 之前,其他线程已经调用了 notify

  4. 死锁:使用 notify 但唤醒的线程不满足条件,又没有其他线程唤醒。

  5. 忽略中断:没有正确处理 InterruptedException,导致线程无法响应中断。

  6. 在 wait() 之后修改了共享变量但没有重新检查条件(如果不用 while,就会有问题)。

面试考点

  1. 手写生产者-消费者模型(经典题)。

  2. wait 和 sleep 的区别(高频题)。

  3. 为什么 wait 必须在同步块中? —— 为了保证线程安全,避免丢失更新和信号丢失。

  4. 虚假唤醒是什么?如何避免? —— 用 while 循环检查条件。

  5. notify 和 notifyAll 的区别及应用场景

  6. 线程状态变化:调用 wait 后线程进入什么状态?被唤醒后进入什么状态?

  7. 锁的释放wait 释放锁,sleep 不释放锁。

  8. 死锁分析:给出一个使用 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 唤醒一个,但被唤醒的线程条件不满足,再次等待,导致其他线程永远没机会被唤醒。

死锁条件

  1. 互斥

  2. 持有并等待

  3. 不可剥夺

  4. 循环等待

wait/notify 可以打破“不可剥夺”条件(因为 wait 释放锁),但如果不正确使用,仍可能因信号丢失导致逻辑上的死锁(线程永远等待)。

总结

以上是对 wait/notify 常见面试考点的详细讲解。在面试中,不仅要能写出代码,还要能解释背后的原理和细节,特别是:

  • 为什么用 while

  • 为什么用 notifyAll

  • 虚假唤醒

  • 锁的释放

  • 与 sleep 的区别

好啦~以上就是本篇的全部内容啦~ 全是干货~~

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐