杰瑞科技汇

Java线程wait/notify如何正确协作?

核心概念:为什么需要 wait()notify()

想象一个生产者-消费者的场景:

Java线程wait/notify如何正确协作?-图1
(图片来源网络,侵删)
  • 生产者:负责生产数据,并放入一个缓冲区(比如一个队列)。
  • 消费者:负责从缓冲区取出数据并进行处理。

这里存在一个竞态条件

  1. 如果缓冲区满了,生产者必须等待,直到消费者取走数据,腾出空间。
  2. 如果缓冲区是空的,消费者必须等待,直到生产者放入新数据。

如果使用简单的 if 判断,会发生什么?

  • 虚假唤醒:线程可能在没有任何通知的情况下,意外地从等待状态中唤醒,如果使用 if,它被唤醒后会直接执行后续代码,而不会再次检查条件,这可能导致数据不一致或错误。
  • 竞态窗口:在 if 条件判断为真后,到 wait() 调用前,另一个线程可能已经修改了条件,导致当前线程进入不必要的等待。

为了解决这些问题,Java 提供了基于监视器的等待/通知机制。


wait() 方法

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

Java线程wait/notify如何正确协作?-图2
(图片来源网络,侵删)
  1. 释放当前对象的锁:这是最关键的一点,调用 wait() 的线程必须先获取到该对象的锁,否则会抛出 IllegalMonitorStateException 异常。
  2. 进入该对象的等待队列:线程会进入该对象的 wait set,并暂时停止执行。
  3. 等待被唤醒:它会一直阻塞,直到发生以下情况之一:
    • 另一个线程调用了该对象的 notify()notifyAll() 方法。
    • 另一个线程中断了该等待线程。
    • 发生了虚假唤醒

wait() 的三个版本

  1. void wait(): 无限期等待,直到被 notify()notifyAll() 唤醒。
  2. void wait(long timeout): 等待指定毫秒数,如果超时,线程会自动被唤醒(就像被 notify() 了一样)。
  3. void wait(long timeout, int nanos): 等待指定毫秒和纳秒数,更精确的超时控制。

最佳实践wait() 总是应该放在一个 while 循环中,而不是 if 语句中,这是为了防止虚假唤醒,确保线程在被唤醒后,再次检查等待条件是否仍然满足。


notify()notifyAll() 方法

这两个方法用于唤醒在某个对象上等待的线程,调用它们的线程必须已经获取到该对象的锁,否则会抛出 IllegalMonitorStateException 异常。

notify()

  • 作用:随机唤醒在该对象等待队列中的一个线程,具体唤醒哪个线程,是由 JVM 决定的,具有不确定性。
  • 特点:它只唤醒一个线程,如果唤醒的线程发现条件仍然不满足(缓冲区仍然是空的),它会再次调用 wait() 并继续等待,这可能会导致“活锁”(Livelock)的情况,即某些线程一直无法获得执行机会。

notifyAll()

  • 作用:唤醒在该对象等待队列中的所有等待线程。
  • 特点:所有被唤醒的线程都会去竞争该对象的锁,只有一个线程能竞争成功并继续执行,其他未成功的线程会再次进入阻塞状态,等待下一次锁的释放。
  • 优点notifyAll() 更安全,因为它能确保至少有一个线程(其等待条件被满足的线程)能够继续执行,避免了 notify() 可能导致的线程饥饿问题。

经典模式:while 循环检查

这是使用 wait/notify 的黄金法则,无论是生产者还是消费者,其等待逻辑都应该遵循这个模式。

// 获取锁
synchronized (lockObject) {
    // 1. 检查等待条件,条件不满足则等待
    while (!conditionIsMet) {
        try {
            // 2. 调用 wait() 释放锁并进入等待状态
            lockObject.wait();
        } catch (InterruptedException e) {
            // 处理中断
            Thread.currentThread().interrupt();
            return;
        }
    }
    // 3. 条件满足,执行核心业务逻辑
    // ... do something ...
}

为什么必须是 while 循环?

Java线程wait/notify如何正确协作?-图3
(图片来源网络,侵删)
  1. 防止虚假唤醒:JVM 规范允许在没有明确通知的情况下,线程被唤醒。while 循环可以确保即使被虚假唤醒,线程也会再次检查条件,如果条件不满足,就继续等待。
  2. 防止条件变化:在 if 判断为真后,到 wait() 调用前,另一个线程可能已经修改了条件,导致当前线程进入不必要的等待。while 循环可以捕获到这种变化。

代码示例:生产者-消费者模型

下面是一个使用 wait()notifyAll() 实现的生产者-消费者模型。

import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
class ProducerConsumerExample {
    // 共享资源,使用一个队列作为缓冲区
    private final Queue<Integer> buffer = new LinkedList<>();
    // 缓冲区最大容量
    private final int CAPACITY = 5;
    public static void main(String[] args) {
        ProducerConsumerExample example = new ProducerConsumerExample();
        // 启动生产者线程
        new Thread(example.new Producer(), "Producer-Thread").start();
        // 启动消费者线程
        new Thread(example.new Consumer(), "Consumer-Thread").start();
    }
    // 生产者
    class Producer implements Runnable {
        private final Random random = new Random();
        @Override
        public void run() {
            try {
                while (true) {
                    produce(random.nextInt(100));
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        private void produce(int item) throws InterruptedException {
            // 使用 buffer 对象作为锁
            synchronized (buffer) {
                // 检查缓冲区是否已满,如果满了就等待
                while (buffer.size() == CAPACITY) {
                    System.out.println("Buffer is full, Producer is waiting...");
                    buffer.wait(); // 释放 buffer 的锁,并进入等待
                }
                System.out.println("Producer produced: " + item);
                buffer.add(item);
                // 通知可能在等待的消费者
                buffer.notifyAll();
            }
            // 模拟生产耗时
            Thread.sleep(1000);
        }
    }
    // 消费者
    class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    consume();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        private void consume() throws InterruptedException {
            // 使用 buffer 对象作为锁
            synchronized (buffer) {
                // 检查缓冲区是否为空,如果空了就等待
                while (buffer.isEmpty()) {
                    System.out.println("Buffer is empty, Consumer is waiting...");
                    buffer.wait(); // 释放 buffer 的锁,并进入等待
                }
                int item = buffer.poll();
                System.out.println("Consumer consumed: " + item);
                // 通知可能在等待的生产者
                buffer.notifyAll();
            }
            // 模拟消费耗时
            Thread.sleep(1500);
        }
    }
}

代码解析:

  1. 共享锁buffer 对象被用作所有线程共享的锁。
  2. 生产者逻辑
    • synchronized 块中获取 buffer 的锁。
    • 使用 while (buffer.size() == CAPACITY) 检查缓冲区是否已满。
    • 如果满了,调用 buffer.wait() 释放锁并等待。
    • 当被唤醒后,while 循环再次检查条件,防止虚假唤醒。
    • 生产数据后,调用 buffer.notifyAll() 唤醒消费者。
  3. 消费者逻辑
    • 与生产者类似,但检查条件是 while (buffer.isEmpty())
    • 如果为空,调用 buffer.wait() 等待。
    • 消费数据后,调用 buffer.notifyAll() 唤醒生产者。

wait() vs sleep() vs yield()

这是一个常见的面试题,三者的区别非常重要。

特性 wait() sleep() yield()
所属类 Object Thread Thread
锁行为 释放锁 不释放锁 不释放锁
使用位置 必须在 synchronized 代码块/方法中 可以在任何地方调用 可以在任何地方调用
唤醒方式 notify()/notifyAll() 唤醒 指定时间后自动唤醒 让出 CPU,调度器决定是否立即重新调度
目的 线程间协作,等待某个条件满足 让线程暂停一段时间,不占用 CPU 提示调度器“我不忙,可以让其他线程运行”

一个常见的误区sleep() 不会释放锁,如果一个线程在 synchronized 代码块中调用了 Thread.sleep(1000),那么在这 1 秒内,其他任何线程都无法进入这个 synchronized 代码块,即使它们不需要锁。


现代替代方案:java.util.concurrent

虽然 wait/notify 是基础,但在现代 Java 开发中,更推荐使用 java.util.concurrent 包中的工具,它们更安全、更高效,也更容易使用。

  • BlockingQueue:这是一个接口,如 ArrayBlockingQueueLinkedBlockingQueue,它们内部已经实现了生产者-消费者的逻辑,你无需手动调用 wait()notify()
    • put(item):如果队列满,会自动阻塞,直到有空间。
    • take():如果队列空,会自动阻塞,直到有元素。
  • Condition 接口:这是 Lock 接口的一部分,提供了更灵活的等待/通知机制,可以创建多个等待队列,而 wait/notify 只能有一个。

使用 BlockingQueue 的生产者-消费者示例:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class ProducerConsumerModern {
    private final BlockingQueue<Integer> queue;
    public ProducerConsumerModern(int capacity) {
        this.queue = new ArrayBlockingQueue<>(capacity);
    }
    public static void main(String[] args) {
        ProducerConsumerModern example = new ProducerConsumerModern(5);
        new Thread(example.new Producer()).start();
        new Thread(example.new Consumer()).start();
    }
    class Producer implements Runnable {
        private final Random random = new Random();
        @Override
        public void run() {
            try {
                while (true) {
                    int item = random.nextInt(100);
                    System.out.println("Producer produced: " + item);
                    queue.put(item); // 如果队列满,这里会自动阻塞
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    int item = queue.take(); // 如果队列空,这里会自动阻塞
                    System.out.println("Consumer consumed: " + item);
                    Thread.sleep(1500);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

可以看到,代码简洁得多,无需手动管理锁和等待/通知逻辑。

  1. 核心用途wait()notify() 用于线程间的协作,一个线程需要等待另一个线程满足特定条件。
  2. 必须配合 synchronized 使用:调用 wait() 的线程必须持有锁,notify() 也必须在持有锁的线程中调用。
  3. while 循环是关键:永远不要在 if 语句中使用 wait(),要用 while 循环来检查等待条件,以防止虚假唤醒和条件变化。
  4. notify() vs notifyAll()notifyAll() 通常更安全,可以避免线程饥饿。notify() 在特定场景下(如只有一个消费者)性能可能更好,但风险更高。
  5. 现代实践:在大多数情况下,优先使用 java.util.concurrent 包中的高级工具(如 BlockingQueue),它们封装了复杂的并发逻辑,使代码更健壮、更易读,理解 wait/notify 仍然是掌握 Java 并发的基础。
分享:
扫描分享到社交APP
上一篇
下一篇