前言

在多线程编程中,线程间的协作与通信是至关重要的。Java 提供了 wait()notify() 和 notifyAll() 方法来实现线程间的等待和通知机制,这些方法是实现生产者-消费者模式、线程池等高级并发组件的基础。

由于 Java 中的 Object 类是所有类的父类,这些方法被放在 Object 类中,确保每个对象都具备线程间通信的能力。


一、wait() 方法

1.1 方法签名

public final void wait() throws InterruptedException

1.2 核心行为

当一个线程调用共享变量的 wait() 方法时,该线程会被阻塞挂起,直到发生以下情况之一才会返回:

  1. 其他线程调用了该共享对象的 notify() 或 notifyAll() 方法

  2. 其他线程调用了该线程的 interrupt() 方法(此时抛出 InterruptedException

1.3 重要前提:监视器锁

调用 wait() 方法的线程必须事先获取该对象的监视器锁,否则会抛出 IllegalMonitorStateException 异常。

1.4 如何获取监视器锁?

方式一:synchronized 同步代码块
synchronized (sharedObject) {
    // 此处已获取 sharedObject 的监视器锁
    sharedObject.wait();
}
方式二:synchronized 实例方法
public synchronized void method() {
    // 当前对象 this 的监视器锁已被获取
    this.wait();
}

1.5 虚假唤醒(Spurious Wakeup)

一个线程从挂起状态变为可运行状态(被唤醒),即使没有被其他线程调用 notify()notifyAll(),也没有被中断或超时,这种情况称为虚假唤醒

⚠️ 重要:虽然虚假唤醒在实际应用中很少发生,但必须防范!

1.6 正确的 wait() 使用模式

synchronized (obj) {
    while (条件不满足) {
        obj.wait();
    }
    // 执行条件满足后的操作
}

为什么使用 while 循环?

  • 防止虚假唤醒

  • 当线程被唤醒后,重新检查条件是否满足

  • 条件不满足则继续等待,直到真正满足条件


二、生产者和消费者示例

下面通过一个完整的示例来加深理解:

public class ProducerConsumerDemo {

    private static final Queue<String> queue = new LinkedList<>();
    private static final int MAX_SIZE = 1;

    /**
     * 生产者线程
     */
    static class Producer extends Thread {
        @Override
        public void run() {
            synchronized (queue) {
                // 队列已满,等待消费者消费
                while (queue.size() == MAX_SIZE) {
                    try {
                        System.out.println("队列已满,生产者等待...");
                        // 释放锁并等待
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                // 生产元素
                String element = "元素-" + System.currentTimeMillis();
                queue.add(element);
                System.out.println("生产: " + element);
                
                // 通知消费者
                queue.notifyAll();
            }
        }
    }

    /**
     * 消费者线程
     */
    static class Consumer extends Thread {
        @Override
        public void run() {
            synchronized (queue) {
                // 队列为空,等待生产者生产
                while (queue.isEmpty()) {
                    try {
                        System.out.println("队列为空,消费者等待...");
                        // 释放锁并等待
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                // 消费元素
                String element = queue.poll();
                System.out.println("消费: " + element);
                
                // 通知生产者
                queue.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        Producer producer = new Producer();
        Consumer consumer = new Consumer();
        
        producer.start();
        consumer.start();
    }
}

为什么 wait() 要释放锁?

如果线程 A 调用 wait() 后不释放锁,那么:

  • 其他生产者和所有消费者线程都被阻塞

  • 线程 A 也被挂起

  • 整个系统陷入死锁状态

释放锁是为了打破死锁必要条件中的持有并等待原则。


三、wait(long timeout) 方法

3.1 方法签名

public final native void wait(long timeout) throws InterruptedException

3.2 与 wait() 的区别

特性 wait() wait(timeout)
超时机制 无,需要被唤醒 超时后自动返回
超时参数 不支持 支持(毫秒)
timeout=0 - 等同于 wait()
负值参数 - 抛出 IllegalArgumentException

3.3 使用示例

synchronized (lock) {
    while (condition) {
        // 最多等待 5000 毫秒
        lock.wait(5000);
        // 超时返回后,需要重新检查条件
    }
}

四、notify() 方法

4.1 方法签名

public final native void notify()

4.2 核心特点

  1. 随机唤醒:唤醒一个在该对象上等待的线程(随机)

  2. 不立即执行:被唤醒的线程需要重新获取监视器锁后才能继续执行

  3. 需要锁:调用 notify() 的线程也必须持有该对象的监视器锁

4.3 执行流程示意图

线程A获得锁 → 调用wait() → 释放锁并进入等待队列
                                    ↓
线程B获得锁 → 调用notify() → 唤醒线程A(从等待队列移到锁竞争队列)
                                    ↓
线程B释放锁 → 线程A竞争锁 → 获取锁后从wait()返回

五、notifyAll() 方法

5.1 方法签名

public final native void notifyAll()

5.2 与 notify() 的对比

方法 唤醒数量 适用场景
notify() 1个线程 多个线程等待同一条件,且条件变化后只需唤醒一个
notifyAll() 所有等待线程 多个线程等待不同条件,或无法确定唤醒哪个

5.3 完整对比示例

public class NotifyVsNotifyAllDemo {

    private static final Object resource = new Object();

    static class WaitingThread extends Thread {
        private final String name;
        
        WaitingThread(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            synchronized (resource) {
                try {
                    System.out.println(name + " 获取锁,开始等待...");
                    resource.wait();
                    System.out.println(name + " 被唤醒,继续执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 启动两个等待线程
        new WaitingThread("线程A").start();
        new WaitingThread("线程B").start();
        
        Thread.sleep(1000);
        
        // 使用 notify() - 只唤醒一个
        synchronized (resource) {
            System.out.println("\n使用 notify(),只唤醒一个线程");
            resource.notify();
        }
        
        Thread.sleep(2000);
        
        // 再次启动两个等待线程
        new WaitingThread("线程C").start();
        new WaitingThread("线程D").start();
        
        Thread.sleep(1000);
        
        // 使用 notifyAll() - 唤醒全部
        synchronized (resource) {
            System.out.println("\n使用 notifyAll(),唤醒所有线程");
            resource.notifyAll();
        }
    }
}

输出结果:

线程A 获取锁,开始等待...
线程B 获取锁,开始等待...

使用 notify(),只唤醒一个线程
线程A 被唤醒,继续执行

使用 notifyAll(),唤醒所有线程
线程C 被唤醒,继续执行
线程D 被唤醒,继续执行

六、中断响应

当一个线程因调用 wait() 方法被阻塞时,如果其他线程中断了该线程,会抛出 InterruptedException

public class WaitInterruptDemo {

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waitingThread = new Thread(() -> {
            try {
                synchronized (lock) {
                    System.out.println("线程开始等待...");
                    lock.wait();
                    System.out.println("这行不会被执行");
                }
            } catch (InterruptedException e) {
                System.out.println("线程被中断,捕获 InterruptedException");
                e.printStackTrace();
            }
        });

        waitingThread.start();
        
        // 等待1秒确保线程进入等待状态
        Thread.sleep(1000);
        
        System.out.println("主线程中断等待线程");
        waitingThread.interrupt();
    }
}

输出结果:

线程开始等待...
主线程中断等待线程
线程被中断,捕获 InterruptedException
java.lang.InterruptedException
    at java.lang.Object.wait(Native Method)
    ...

七、常见陷阱与最佳实践

7.1 常见异常

异常 原因 解决方案
IllegalMonitorStateException 未在同步块内调用 wait/notify 确保在 synchronized 内部调用
IllegalArgumentException wait(timeout) 参数为负数 检查超时参数

7.2 最佳实践总结

// ✅ 正确写法
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    // 执行操作
}

// ❌ 错误写法1:没有获取锁
lock.wait();  // 抛出 IllegalMonitorStateException

// ❌ 错误写法2:使用 if 而不是 while
synchronized (lock) {
    if (!condition) {  // 无法防止虚假唤醒
        lock.wait();
    }
}

7.3 核心要点

  1. 始终在同步块内调用 wait/notify

  2. 始终使用 while 循环条件判断

  3. 优先使用 notifyAll() 而非 notify()

  4. 处理 InterruptedException 异常

  5. wait() 会释放锁,notify() 不会释放锁


八、总结

方法 作用 释放锁 需要锁
wait() 阻塞当前线程 ✅ 是 ✅ 是
wait(timeout) 限时阻塞 ✅ 是 ✅ 是
notify() 唤醒一个等待线程 ❌ 否 ✅ 是
notifyAll() 唤醒所有等待线程 ❌ 否 ✅ 是

核心记忆口诀:

同步块内调等待,while 循环防虚假。
通知唤醒不释锁,等待释锁是关键。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐