基于本文回答
0
评论

深入理解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(),造成死锁。


现代替代方案:LockCondition

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

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

Condition 的优势:

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

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

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 包下的 LockCondition,因为它们更灵活、功能更强大。
右滑查看面试常问