基于本文回答

播面 播面

文解图解播客视频,多维讲透八股文

深入理解wait/notify/notifyAll

知识点图片

本文讲解Java线程协作工具wait/notify,重点阐述了其必须在synchronized块和while循环中使用的核心规则。

我们来深入、详细地讲解一下Java中的wait()、notify()和notifyAll()。这三个方法是Java多线程协作的核心机制,但也是初学者容易出错的地方。

核心概念:监视器锁 (Monitor Lock)

在理解wait/notify之前,必须先理解Java的内置锁机制,也叫做监视器锁 (Monitor Lock)。

  1. 每个对象都有一个锁:在Java中,任何一个对象(new Object())都可以作为一个锁。
  2. synchronized关键字:当一个线程通过synchronized关键字获取了一个对象的锁之后,其他线程就无法再进入该对象上任何其他的synchronized代码块或方法,直到该线程释放锁。
  3. 锁的归属:这个锁是属于对象的,而不是属于线程或代码块的。

wait()、notify()和notifyAll()就是围绕着这个对象监视器锁进行协作的工具。


wait(), notify(), notifyAll() 是什么?

这三个方法都定义在 java.lang.Object 类中。这意味着任何Java对象都可以调用这三个方法。

  • wait():

    • 当一个线程调用某个对象的 wait() 方法时,它会立即释放该对象的锁。
    • 然后,该线程进入等待状态 (WAITING),并被放入该对象的等待队列 (Wait Set)中。
    • 它会一直等待,直到其他线程调用同一个对象的 notify() 或 notifyAll() 方法。
  • notify():

    • 当一个线程调用某个对象的 notify() 方法时,它会从该对象的等待队列中随机唤醒一个正在等待的线程。
    • 被唤醒的线程并不会立即执行,而是进入就绪状态 (RUNNABLE),需要重新竞争该对象的锁。只有当它成功获取到锁之后,才能从当初调用 wait() 的地方继续执行。
  • notifyAll():

    • 与 notify() 类似,但它会唤醒该对象等待队列中所有正在等待的线程。
    • 所有被唤醒的线程会一起去竞争该对象的锁。

使用 wait/notify 的三大黄金法则

这是最重要的部分,违反任何一条都会导致程序出错,通常是抛出 IllegalMonitorStateException。

法则一:必须在 synchronized 代码块或方法中使用

wait()、notify()、notifyAll() 的调用者必须是当前 synchronized 锁定的对象。

为什么?
wait/notify 机制的本质是协调持有锁的线程和等待锁的线程。如果你都没有获取到对象的锁,凭什么去释放它(wait())或者通知其他等待该锁的线程(notify())呢?所以,调用这三个方法前,必须先通过 synchronized 获得该对象的监视器锁。

正确示例:

java
Object lock = new Object();

synchronized (lock) {
    // 在这里调用 lock.wait() 或 lock.notify() 是合法的
    lock.wait();
}

错误示例:

java
Object lock = new Object();
// 错误!当前线程没有持有 lock 对象的锁
lock.wait(); // 会抛出 IllegalMonitorStateException

法则二:调用 wait() / notify() 的对象必须与 synchronized 锁定的对象是同一个

这一点是法则一的延伸,但非常容易搞错。

正确示例:

java
public class MyService {
    private Object lock = new Object();

    public void doWait() {
        synchronized (lock) { // 锁的是 lock 对象
            try {
                lock.wait(); // 在 lock 对象上等待,正确!
            } catch (InterruptedException e) {
                // ...
            }
        }
    }

    public void doNotify() {
        synchronized (lock) { // 锁的是 lock 对象
            lock.notify(); // 在 lock 对象上通知,正确!
        }
    }
}

错误示例:

java
public class MyService {
    private Object lock = new Object();

    public synchronized void doWait() { // 锁的是 this 对象
        try {
            // 错误!锁是this,却在lock对象上等待
            lock.wait(); // 抛出 IllegalMonitorStateException
        } catch (InterruptedException e) {
            // ...
        }
    }
}

法则三:wait() 方法必须放在 while 循环中

为什么?
这主要是为了防止虚假唤醒 (Spurious Wakeup)。操作系统或JVM有时可能会在没有任何线程调用notify()或notifyAll()的情况下唤醒一个等待的线程。这虽然罕见,但确实可能发生。

如果使用 if,线程被虚假唤醒后会直接往下执行,此时它等待的条件可能并未满足,从而导致程序错误。而使用 while 循环,线程被唤醒后会重新检查等待的条件,如果条件不满足,它会再次调用 wait() 进入等待状态,从而保证了程序的健壮性。

正确示例(生产者-消费者模型中的消费者):

java
Queue<String> buffer = new LinkedList<>();
int capacity = 5;

// ...

synchronized (buffer) {
    // 使用 while 循环检查条件
    while (buffer.isEmpty()) {
        try {
            System.out.println("消费者等待中...");
            buffer.wait(); // 缓冲区为空,等待
        } catch (InterruptedException e) {
            // ...
        }
    }
    // 当循环结束,说明条件(buffer不为空)已经满足
    String item = buffer.poll();
    System.out.println("消费者消费了: " + item);
    // ...
}

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

这个例子最能体现 wait/notify 的作用。

java
import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumerExample {

    public static void main(String[] args) {
        Queue<Integer> buffer = new LinkedList<>();
        int capacity = 5;

        Thread producerThread = new Thread(new Producer(buffer, capacity), "Producer");
        Thread consumerThread = new Thread(new Consumer(buffer), "Consumer");

        producerThread.start();
        consumerThread.start();
    }
}

// 生产者
class Producer implements Runnable {
    private final Queue<Integer> buffer;
    private final int capacity;

    public Producer(Queue<Integer> buffer, int capacity) {
        this.buffer = buffer;
        this.capacity = capacity;
    }

    @Override
    public void run() {
        int value = 0;
        while (true) {
            synchronized (buffer) {
                // 使用 while 循环检查条件
                while (buffer.size() == capacity) {
                    try {
                        System.out.println("缓冲区已满,生产者等待...");
                        buffer.wait(); // 释放锁,进入等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println("生产者生产了: " + value);
                buffer.add(value++);
                
                // 唤醒可能在等待的消费者
                buffer.notifyAll(); 
            }
            try {
                // 模拟生产耗时
                Thread.sleep(100); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 消费者
class Consumer implements Runnable {
    private final Queue<Integer> buffer;

    public Consumer(Queue<Integer> buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (buffer) {
                // 使用 while 循环检查条件
                while (buffer.isEmpty()) {
                    try {
                        System.out.println("缓冲区为空,消费者等待...");
                        buffer.wait(); // 释放锁,进入等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                int value = buffer.poll();
                System.out.println("消费者消费了: " + value);
                
                // 唤醒可能在等待的生产者
                buffer.notifyAll();
            }
             try {
                // 模拟消费耗时
                Thread.sleep(500); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

为什么推荐使用 notifyAll() 而不是 notify()?

在上面的例子中,如果同时有多个生产者和多个消费者,notify() 可能会导致问题。
想象一下:

  1. 缓冲区满了,所有生产者都在wait()。
  2. 一个消费者消费了一项,调用了notify()。
  3. 这个notify()不幸唤醒了另一个生产者。
  4. 这个被唤醒的生产者检查 while (buffer.size() == capacity),发现缓冲区还是满的(因为刚刚消费了一个,但可能马上又被其他生产者填满了,或者它自己醒来时还没空位),于是它又wait()了。
  5. 结果就是,一个本可以进行的消费操作没有被唤醒,造成了信号丢失,所有线程都可能永久等待下去,导致死锁。

而 notifyAll() 会唤醒所有等待的线程(包括所有生产者和所有消费者),它们都会去重新检查条件。虽然会带来一些不必要的竞争(“惊群效应”),但它能确保正确的线程(在这个例子中是消费者)最终会被唤醒并继续工作,程序逻辑是安全的。


与 Thread.sleep() 的区别

特性 wait() Thread.sleep()
所属类 Object Thread
是否释放锁 是,会释放持有的监视器锁 否,不会释放任何锁
唤醒方式 需要被其他线程通过 notify() 或 notifyAll() 唤醒 时间到了自动唤醒
使用场景 线程间的协作、通信 暂停当前线程的执行,不涉及协作
必要条件 必须在 synchronized 块或方法中调用 可以在任何地方调用

wait() 释放锁是它和 sleep() 最本质的区别,这使得其他线程有机会获取锁并改变条件,然后唤醒等待的线程。如果 wait() 不释放锁,那么其他线程将永远无法进入 synchronized 代码块,也就无法调用 notify(),造成死锁。


现代替代方案:Lock 和 Condition

在 java.util.concurrent.locks 包下,Java 提供了更强大、更灵活的锁机制。

  • Lock 接口(如 ReentrantLock)替代 synchronized。
  • Condition 接口替代 Object 的 wait/notify 机制。

Condition 的优势:

  1. 多个等待队列:一个 Lock 可以创建多个 Condition 对象。例如,在生产者-消费者模型中,可以创建一个 "缓冲区非满" 的条件和一个 "缓冲区非空" 的条件。生产者只在 "非满" 条件上等待,消费者只在 "非空" 条件上等待。当生产者生产后,只需唤醒等待 "非空" 条件的消费者,而不是所有线程,效率更高。
  2. 更丰富的功能:提供了await(), signal(), signalAll(),分别对应wait(), notify(), notifyAll()。此外还支持可中断的等待、定时的等待等。

使用 Lock 和 Condition 的生产者-消费者示例:

java
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ProducerConsumerWithLock {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition(); // 生产者等待的条件
    private final Condition notEmpty = lock.newCondition(); // 消费者等待的条件
    private final Queue<Integer> buffer = new LinkedList<>();
    private final int capacity = 5;

    public void produce() throws InterruptedException {
        int value = 0;
        while (true) {
            lock.lock(); // 获取锁
            try {
                while (buffer.size() == capacity) {
                    System.out.println("生产者等待...");
                    notFull.await(); // 在 notFull 条件上等待
                }
                buffer.add(value);
                System.out.println("生产了: " + value++);
                notEmpty.signalAll(); // 唤醒所有在 notEmpty 上等待的消费者
            } finally {
                lock.unlock(); // 必须在 finally 中释放锁
            }
            Thread.sleep(100);
        }
    }

    public void consume() throws InterruptedException {
         while (true) {
            lock.lock();
            try {
                while (buffer.isEmpty()) {
                    System.out.println("消费者等待...");
                    notEmpty.await(); // 在 notEmpty 条件上等待
                }
                int value = buffer.poll();
                System.out.println("消费了: " + value);
                notFull.signalAll(); // 唤醒所有在 notFull 上等待的生产者
            } finally {
                lock.unlock();
            }
            Thread.sleep(500);
        }
    }
}

总结

  • wait/notify/notifyAll 是Java中用于线程间通信和协作的底层机制。
  • 它们必须与 synchronized 关键字配合使用,并且调用对象和锁对象必须是同一个。
  • wait() 会释放锁并让线程进入等待状态。
  • wait() 必须放在 while 循环中以防止虚假唤醒。
  • 为避免死锁,优先使用 notifyAll() 而不是 notify()。
  • 对于新的或复杂的并发场景,推荐使用 java.util.concurrent.locks 包下的 Lock 和 Condition,因为它们更灵活、功能更强大。