杰瑞科技汇

Java线程wait/notify为何必须加锁?

这两个方法是 Java 中实现线程间通信协作的核心,属于 Object 类的方法,而不是 Thread 类,这是非常重要的一个点。

Java线程wait/notify为何必须加锁?-图1
(图片来源网络,侵删)

为什么需要 wait()notify()

在多线程编程中,我们经常遇到这样的场景:一个线程需要等待另一个线程完成某个任务后,才能继续执行。

  • 不使用 wait/notify 的问题:如果使用一个简单的 while 循环来轮询(不断检查)某个条件,会导致 CPU 空转,浪费大量计算资源。

    // 糟糕的实现方式
    while (!condition) {
        // 什么也不做,只是空转
    }
    // 当 condition 变为 true 时,继续执行

    这种方式极其低效,因为它占用了 CPU 时间片,却没有做任何有意义的工作。

  • wait/notify 的解决方案wait()notify() 提供了一种高效的机制,让等待的线程进入阻塞(Blocked)状态,释放 CPU 资源,当条件满足时,由另一个线程将其唤醒,使其从阻塞状态返回,继续执行。

    Java线程wait/notify为何必须加锁?-图2
    (图片来源网络,侵删)

wait() 方法

当一个线程调用某个对象的 wait() 方法时,它会做以下几件事:

  1. 释放当前对象的锁:这是最关键的一点,调用 wait() 的线程必须已经获得了该对象的锁(即它必须在同步代码块或同步方法中调用)。
  2. 进入该对象的等待队列:线程会进入该对象的 wait set,并立即释放锁。
  3. 进入阻塞状态:线程会一直等待,直到其他线程调用该对象的 notify()notifyAll() 方法来唤醒它。
  4. 被唤醒后重新获取锁:当线程被唤醒后,它会尝试重新获取之前释放的锁,一旦成功获取锁,它才会从 wait() 方法调用处继续执行。

wait() 的重载方法

  • void wait(): 无限等待,直到被 notify()notifyAll() 唤醒。
  • void wait(long timeout): 等待指定的毫秒数,如果超时了,线程会自动被唤醒(抛出 InterruptedException 也会被唤醒)。
  • void wait(long timeout, int nanos): 等待指定的毫秒和纳秒数。

重要:被唤醒的线程并不代表条件一定满足了!它只是获得了重新运行的机会,被唤醒的线程必须在一个 while 循环中再次检查条件,这被称为“伪唤醒”(Spurious Wakeup),虽然不常见,但 JVM 规范允许其发生。


notify()notifyAll() 方法

这两个方法由持有锁的线程调用,用于唤醒正在等待的线程。

notify()

  • 作用:随机唤醒一个正在等待该对象锁的线程。
  • 选择哪个线程:这个选择是不固定的,由 JVM 决定,你无法指定唤醒哪个特定的线程。
  • 场景:当你确定只有一个线程在等待,或者你“不关心”具体唤醒哪个线程时使用。

notifyAll()

  • 作用:唤醒所有正在等待该对象锁的线程。
  • 后续执行:所有被唤醒的线程都会去竞争对象的锁,只有一个线程能竞争成功并继续执行,其他的线程则再次进入阻塞状态,等待下一次获取锁。
  • 场景:当你有多个线程在等待,并且需要根据某个具体条件来决定哪个线程应该继续执行时,notifyAll() 是更安全的选择,它通过让所有线程都去检查条件,来确保逻辑的正确性。

使用 wait/notify 的核心原则(必须遵守)

wait()notify() 必须在同步代码块或同步方法中使用,并且必须使用同一个对象锁

以下是必须遵守的三个核心原则:

  1. 必须在同步块/方法中调用:调用 wait()notify() 的线程必须先获取到该对象的锁。
  2. 必须使用同一个锁对象wait(), notify(), notifyAll() 必须作用于同一个对象上。
  3. 永远在 while 循环中调用 wait():不要使用 if 语句,因为存在“伪唤醒”的可能,while 循环可以确保在条件不满足时,线程会再次调用 wait(),继续等待。

经典示例:生产者-消费者模型

这个模型是 wait/notify 最经典的应用场景。

import java.util.LinkedList;
import java.util.Queue;
class ProducerConsumerExample {
    // 共享资源,使用同一个锁对象
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 5;
    // 生产者
    class Producer implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    // 如果队列满了,生产者等待
                    while (queue.size() == MAX_SIZE) {
                        System.out.println("队列已满,生产者等待...");
                        try {
                            queue.wait(); // 释放 queue 的锁,并进入等待
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 队列未满,生产数据
                    int value = (int) (Math.random() * 100);
                    queue.add(value);
                    System.out.println("生产: " + value + " | 队列大小: " + queue.size());
                    // 通知消费者(可能有多个消费者在等待,所以用 notifyAll)
                    queue.notifyAll(); // 唤醒在 queue 上等待的线程
                }
                try {
                    Thread.sleep(1000); // 模拟生产耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 消费者
    class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    // 如果队列空了,消费者等待
                    while (queue.isEmpty()) {
                        System.out.println("队列为空,消费者等待...");
                        try {
                            queue.wait(); // 释放 queue 的锁,并进入等待
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 队列非空,消费数据
                    int value = queue.poll();
                    System.out.println("消费: " + value + " | 队列大小: " + queue.size());
                    // 通知生产者(生产者可能在等待)
                    queue.notifyAll(); // 唤醒在 queue 上等待的线程
                }
                try {
                    Thread.sleep(1500); // 模拟消费耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) {
        ProducerConsumerExample example = new ProducerConsumerExample();
        Thread producerThread = new Thread(example.new Producer());
        Thread consumerThread = new Thread(example.new Consumer());
        producerThread.start();
        consumerThread.start();
    }
}

示例解析:

  1. 共享资源queue 是生产者和消费者共享的队列,MAX_SIZE 是队列的最大容量。
  2. 锁对象synchronized (queue),所有对 queue 的操作都使用 queue 对象作为锁。
  3. 生产者逻辑
    • 在同步块中,先检查 while (queue.size() == MAX_SIZE)
    • 如果满了,调用 queue.wait() 释放锁并等待。
    • 如果没满,就生产数据并添加到队列。
    • 调用 queue.notifyAll() 唤醒可能在等待的消费者。
  4. 消费者逻辑
    • 在同步块中,先检查 while (queue.isEmpty())
    • 如果空了,调用 queue.wait() 释放锁并等待。
    • 如果不空,就从队列中取出数据消费。
    • 调用 queue.notifyAll() 唤醒可能在等待的生产者。
  5. 为什么用 notifyAll() 而不是 notify()
    • 假设有两个消费者线程都在等待,如果生产者调用 notify(),它只能随机唤醒一个消费者,另一个消费者会继续等待,即使生产者已经生产了新数据,而 notifyAll() 会唤醒所有消费者,让它们去竞争锁,确保消费能顺利进行,在更复杂的场景下,notifyAll() 能避免因唤醒错误的线程而导致的死锁或逻辑错误。

wait/notify vs. java.util.concurrent

虽然 wait/notify 是 Java 线程通信的基础,但在现代 Java 开发中,我们更推荐使用 java.util.concurrent 包中的高级工具,因为它们

分享:
扫描分享到社交APP
上一篇
下一篇