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() 方法时,就标志着你已经从“会写线程”进阶到“懂线程协作”了。接下来,你可以学习 ReentrantLockSemaphore 等更高级的并发工具,进一步提升你的并发编程能力。

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