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 类的方法,有三个重载版本:

java 复制代码
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 实现中,即使没有调用 notifynotifyAll,等待的线程也可能被意外唤醒。

  • 这种现象称为虚假唤醒,是底层系统的行为,无法完全避免。

5.2 正确的做法

因此,wait() 必须放在一个 while 循环 中,循环检查条件 ,而不是用 if 判断一次。这样可以保证即使被虚假唤醒,也会重新检查条件,如果条件不满足就继续等待。

java 复制代码
synchronized (lock) {
    while (条件不满足) {  // 必须用 while
        lock.wait();
    }
    // 条件满足,执行后续操作
}

如果用 if,线程被唤醒后就直接往下执行,如果条件实际上并不满足(虚假唤醒),就会导致逻辑错误。

易错点 :新手常犯错误是用 if 判断条件,忽略了虚假唤醒。

面试考点 :什么是虚假唤醒?为什么 wait 要放在循环中?------考察对线程安全细节的理解。


6. 基础代码示例(逐步深入)

6.1 示例1:最简单的 wait/notify

java 复制代码
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 循环的必要性(模拟虚假唤醒)

java 复制代码
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 唤醒所有等待线程

java 复制代码
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:生产者-消费者模型(经典案例)

java 复制代码
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() 后,其他线程可以进入同步块,因为锁已释放。

面试考点 :手写生产者-消费者代码,并解释为什么用 whilenotifyAll


7. 常见错误与注意事项

7.1 不在同步块中调用 wait/notify

java 复制代码
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 通常与条件变量一起使用,例如:

java 复制代码
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. waitsleep 的区别(高频题)。

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

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

  5. notifynotifyAll 的区别及应用场景

  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 必须在同步块中?

核心原因 :为了保证条件检查和等待操作的原子性,避免发生竞态条件。

反例分析

java 复制代码
// 错误的写法(非同步)
if (!condition) {
    lock.wait(); // 不在同步块中,会抛 IllegalMonitorStateException
}

即使我们假设可以这样写,假设线程 A 检查到 condition 为 false,准备调用 wait(),但此时线程 B 抢先把 condition 设为 true 并调用了 notify(),然后线程 A 才调用 wait(),那么线程 A 就会永远等待下去(信号丢失)。

正确的做法

java 复制代码
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
}

在同步块中,检查条件和调用 wait 是原子的(因为持有锁),其他线程无法在中间修改条件,从而保证了正确性。

补充wait 内部会释放锁,所以进入等待集后,其他线程可以获得锁并修改条件。

4. 虚假唤醒是什么?如何避免?

定义 :虚假唤醒是指一个线程在没有收到 notify/notifyAll 的情况下,从 wait 状态中被唤醒。这是某些操作系统底层的特性,Java 规范允许这种现象发生。

为什么会有虚假唤醒 :为了提高实现效率,一些操作系统的等待/通知机制可能允许线程在特定条件下(如信号中断)被唤醒,但 Java 无法区分这些情况,因此规定 wait 可以无条件返回。

如何避免 :必须将 wait 放在一个 while 循环中,循环检查条件。这样即使发生虚假唤醒,线程会重新检查条件,如果条件不满足,则继续等待。

代码示例

java 复制代码
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 的代码,判断是否可能死锁。

示例

java 复制代码
// 线程 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 的区别

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

相关推荐
2401_831920741 小时前
C++中的桥接模式
开发语言·c++·算法
霍格沃兹测试学院-小舟畅学1 小时前
LangChain + DeepSeek 实战拆解:从 LCEL 到智能体,如何真正“做出”一个可控 AI 系统?
java·人工智能·langchain
Promising_GEO1 小时前
探索Python融合地学:绘制栅格数据经纬度剖面图
开发语言·python·遥感·地理
m0_743470371 小时前
C++中的桥接模式变体
开发语言·c++·算法
IT猿手1 小时前
MATLAB画四旋翼无人机,机翼可独立旋转
开发语言·matlab·无人机
96771 小时前
java数据类型解析以及相关八股文的题 String 到底是基本类型还是引用类型?
java·开发语言·python
会编程的土豆1 小时前
【影院管理系统】
开发语言
gulinigar1 小时前
C++中的观察者模式实战
开发语言·c++·算法
星空露珠1 小时前
迷你世界UGC3.0脚本Wiki对象模块管理接口 GameObject
开发语言·数据库·算法·游戏·lua