Java Object wait() 方法(一文讲透)

Java Object wait() 方法详解:线程同步中的“暂停与唤醒”机制

在 Java 多线程编程中,我们经常需要控制线程之间的协作。比如一个线程在等待某个资源就绪,另一个线程负责提供这个资源。这时候,Java Object wait() 方法 就派上了用场。它不是简单的“暂停”,而是一种线程间的协作机制,让线程在特定条件下主动放弃锁并进入等待状态,直到其他线程显式唤醒它。

如果你对线程同步还停留在 synchronized 关键字的表面理解,那么深入学习 wait() 方法,将是你迈向高级并发编程的重要一步。


wait() 方法的基本语法与使用前提

wait() 是 java.lang.Object 类的实例方法,所有 Java 对象都可以调用。它的主要作用是让当前线程主动释放对象锁并进入等待状态,直到被其他线程通过 notify() 或 notifyAll() 唤醒。

语法形式

public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
  • wait():无限期等待,直到被唤醒。
  • wait(timeout):最多等待指定毫秒数,超时后自动恢复。
  • wait(timeout, nanos):更精细的时间控制,通常不常用。

使用前提(必须满足三个条件)

  1. 必须在 synchronized 同步块或方法中调用
    否则会抛出 IllegalMonitorStateException。这是为了确保调用 wait() 的线程持有该对象的锁。

  2. 调用前必须持有对象的监视器(monitor)
    也就是你必须已经通过 synchronized 获取了对象的锁。

  3. wait() 会释放锁并进入等待队列
    这是关键点——线程不会一直占用锁,而是“主动交出”锁,让其他线程有机会执行。

📌 比喻理解:把对象的锁想象成一个“会议室钥匙”。你拿着钥匙进会议室开会(执行同步代码),但突然发现资料没准备好。这时你调用 wait(),相当于你把钥匙放下,离开会议室,告诉别人:“我等资料好了再回来。” 其他人就可以拿钥匙进来开会了。


实际案例:生产者-消费者模型

让我们用经典的生产者-消费者问题来演示 wait() 方法的实际用法。

假设有一个共享的缓冲区,生产者往里面放数据,消费者从中取数据。如果缓冲区满了,生产者要等;如果为空,消费者也要等。

代码示例

public class ProducerConsumerExample {

    private final Object lock = new Object(); // 用于同步的锁对象
    private int buffer = 0; // 共享缓冲区
    private boolean isEmpty = true; // 标记缓冲区是否为空

    // 生产者线程
    public class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                synchronized (lock) {
                    // 如果缓冲区已满,等待
                    while (!isEmpty) {
                        try {
                            System.out.println("生产者: 缓冲区已满,等待消费者消费...");
                            lock.wait(); // 释放锁,进入等待状态
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            System.out.println("生产者被中断");
                            return;
                        }
                    }

                    // 生产数据
                    buffer = i;
                    isEmpty = false;
                    System.out.println("生产者生产: " + buffer);

                    // 唤醒等待的消费者
                    lock.notifyAll();
                }
            }
        }
    }

    // 消费者线程
    public class Consumer implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i <= 5; i++) {
                synchronized (lock) {
                    // 如果缓冲区为空,等待
                    while (isEmpty) {
                        try {
                            System.out.println("消费者: 缓冲区为空,等待生产者生产...");
                            lock.wait(); // 释放锁,进入等待状态
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            System.out.println("消费者被中断");
                            return;
                        }
                    }

                    // 消费数据
                    int value = buffer;
                    isEmpty = true;
                    System.out.println("消费者消费: " + value);

                    // 唤醒等待的生产者
                    lock.notifyAll();
                }
            }
        }
    }

    // 启动测试
    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();

        try {
            producerThread.join();
            consumerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("生产消费流程结束");
    }
}

代码详解

  • synchronized (lock):确保每次只有一个线程能进入临界区。
  • while (!isEmpty):注意是 while 而不是 if。为什么?
    • 因为线程被唤醒后,可能由于其他线程的干扰导致条件不再满足(即“虚假唤醒”)。
    • 使用 while 可以保证再次判断条件,避免错误执行。
  • lock.wait():释放锁并等待,直到被 notifyAll() 唤醒。
  • lock.notifyAll():唤醒所有在该锁上等待的线程。推荐使用 notifyAll(),而不是 notify(),避免遗漏某个线程。

wait() 与 sleep() 的区别:你真的分得清吗?

很多初学者容易混淆 wait() 和 Thread.sleep()。虽然两者都能让线程“暂停”,但本质完全不同。

特性 wait() sleep()
所属类 Object 类 Thread 类
是否释放锁 ✅ 是 ❌ 否
是否需要 synchronized ✅ 必须 ❌ 不需要
唤醒方式 通过 notify()/notifyAll() 超时自动恢复
适用场景 线程协作、资源等待 简单延时、定时任务

📌 举个例子:
你正在排队买票,前面有 3 个人。

  • sleep(1000) 就像你闭眼数 1 秒,但你还是站在队列里,别人不能插队。
  • wait() 就像你对售票员说:“我先去喝杯水,等我回来再买。” 然后你离开队伍,别人可以去排队买票。

所以,当你需要线程间协作时,永远优先考虑 wait()。


wait() 的常见误区与最佳实践

误区一:在非 synchronized 块中调用 wait()

// ❌ 错误写法
public void wrongMethod() {
    Object obj = new Object();
    obj.wait(); // 抛出 IllegalMonitorStateException
}

✅ 正确做法:必须在 synchronized 块中调用。

误区二:使用 if 而不是 while 判断条件

// ❌ 危险写法
if (isEmpty) {
    lock.wait(); // 可能虚假唤醒,导致误操作
}

✅ 正确做法:始终使用 while 循环判断条件。

误区三:只用 notify() 而不用 notifyAll()

// ❌ 可能导致死锁
lock.notify(); // 只唤醒一个线程,另一个可能永远等不到

✅ 推荐:使用 notifyAll(),确保所有等待线程都有机会被唤醒。


wait() 方法的底层机制简析

wait() 方法在 JVM 层面依赖于监视器(monitor)机制。每个对象都有一个与之关联的监视器,线程进入 synchronized 块时获得该监视器,调用 wait() 时会:

  1. 释放当前持有的监视器锁;
  2. 将线程放入该对象的等待队列;
  3. 进入“WAITING”状态;
  4. 当收到 notify() 或 notifyAll() 时,线程被移出等待队列,但不会立即恢复执行,需要重新竞争监视器锁。

这个过程保证了线程安全和协作的原子性。


如何安全地中断 wait() 线程?

有时候,我们需要在等待过程中中断线程。wait() 方法会抛出 InterruptedException,这是中断机制的体现。

示例:支持中断的 wait

try {
    lock.wait(5000); // 最多等待 5 秒
} catch (InterruptedException e) {
    // 处理中断
    Thread.currentThread().interrupt(); // 重新设置中断标志
    System.out.println("等待被中断,线程退出");
    return;
}

📌 关键点:捕获 InterruptedException 后,应调用 Thread.currentThread().interrupt(),以便上层代码能感知中断状态。


总结:掌握 wait() 方法,打通多线程协作的任督二脉

Java Object wait() 方法 是 Java 并发编程的基石之一。它不仅仅是一个“暂停”工具,更是一种优雅的线程协作协议。通过合理使用 wait()、notify() 和 notifyAll(),我们可以构建出高效、安全的多线程应用。

  • 理解其调用前提:必须在 synchronized 块中。
  • 掌握 while 循环的使用,防止虚假唤醒。
  • 区分 wait() 与 sleep() 的本质差异。
  • 善用 notifyAll() 保证线程安全。
  • 正确处理 InterruptedException。

当你能熟练运用 wait() 方法时,就标志着你已经从“会写线程”进阶到“懂线程协作”了。接下来,你可以学习 ReentrantLock、Semaphore 等更高级的并发工具,进一步提升你的并发编程能力。

最后提醒一句:线程安全,始于细节。每一个 wait() 调用的背后,都是一次对程序逻辑的深度思考。