深入理解 Java 线程协作:彻底搞懂 wait() 与 notify() 的工作机制

在 Java 多线程编程的世界里,wait()notify() 是两个非常基础且至关重要的方法。你一定知道,多线程环境下处理共享数据时,协调线程的执行顺序往往是最让人头疼的问题。这两个方法正是 Java 为我们提供的解决这一难题的“原生钥匙”。它们定义在所有类的始祖 —— Object 类 中,这意味着任何 Java 对象都可以作为线程通信的锁载体。

在深入细节之前,让我们先聊聊它们诞生的背景。引入这两个方法的主要目的是为了摆脱“轮询”这种低效的编程模式。所谓的轮询,就是线程不断地重复检查某个条件是否满足(比如在 INLINECODE7ae114f6 循环中检查标志位)。这种做法虽然简单,但会极大地浪费 CPU 资源,导致“忙等待”。而在实际开发中,我们更希望线程在条件不满足时能够沉睡,不再占用 CPU,直到条件成熟被其他线程唤醒。这正是 INLINECODE5e482664 和 notify() 大显身手的地方。

wait() 方法详解

INLINECODE471f169b 方法是 java.lang.Object 类的一部分。当某个线程调用了对象的 INLINECODEe4c77d6e 方法后,它会立即释放当前持有的对象锁,并停止当前的执行流程,进入等待状态。在这个状态下,它会一直等待,直到其他线程调用了同一个对象的 INLINECODE55be6eeb 或 INLINECODE929abe29 方法来唤醒它,或者等待时间超时。

wait() 方法主要有 3 种重载形式:

#### 1. wait()

这是 INLINECODE8f69ea9b 方法最基础的形式,它不接受任何参数。调用它会导致当前线程无限期地等待,直到其他线程调用了 INLINECODE2ad88d3d 或 notifyAll() 方法。

public final void wait() throws InterruptedException

#### 2. wait(long timeout)

这个版本接受一个毫秒级的超时参数。调用它会导致线程等待,直到被通知唤醒,或者直到超过了指定的超时时间(以先发生者为准)。这种机制可以有效避免线程因为逻辑错误而永久死锁。

public final void wait(long timeout) throws InterruptedException

#### 3. wait(long timeout, int nanos)

这个版本提供了更高精度的控制,它允许你指定毫秒之外的纳秒级时间。虽然在实际 JVM 调度中,纳秒级的精度很难完全保证,但在某些对时间敏感的场景下,它提供了理论上的更高精度。

public final void wait(long timeout, int nanos) throws InterruptedException

notify() 与 notifyAll() 方法

notify() 方法同样定义在 Object 类中。它的作用是唤醒正在等待该对象(锁)的某一个线程。需要注意的是,它具体唤醒哪一个等待线程是由 JVM 调度器随机选择的,我们无法人为指定。

public final void notify()

!wait() 和 notify() 工作流程示意图

图解:展示了线程如何进入等待集,以及如何通过通知被唤醒并重新竞争锁的过程。

此外,还有一个 INLINECODE53b9c4e5 方法。它不是唤醒某一个线程,而是唤醒所有正在等待该对象锁的线程。虽然这听起来更公平,但也会导致所有被唤醒的线程瞬间竞争锁,可能引发“惊群效应”,增加系统开销。通常建议优先使用 INLINECODE2510471c,除非你确实需要唤醒所有等待线程来处理不同的条件。

wait() 和 notify() 的核心差异

为了更清晰地理解这两个方法,让我们通过下面的表格来梳理一下它们之间的核心差异:

序号

Wait()

notify() —

— 1.

功能:导致当前线程释放锁并进入等待状态。

功能:唤醒任意一个正在等待该对象锁的线程。 2.

锁状态:线程调用 wait() 后,会立即释放持有的监视器锁。

锁状态:线程调用 notify() 时,并不立即释放锁。它只是通知等待的线程,锁的释放通常发生在同步代码块执行结束时。 3.

执行顺序:线程从“运行中”变为“等待中”。

执行顺序:被唤醒的线程从“等待中”变为“阻塞(争夺锁)”,只有在获得锁后才能变为“运行中”。 4.

所属类:定义在 java.lang.Object 类中。

所属类:定义在 java.lang.Object 类中。 5.

用途:主要用于线程间的通信或条件等待。

用途:主要用于向等待集发送信号,告知条件可能已改变。 6.

异常:必须捕获 InterruptedException。

异常:没有强制要求捕获异常。

实战代码示例:生产者与消费者模型

光说不练假把式。让我们通过一个真实的代码示例来演示 INLINECODE484ea25c 和 INLINECODEdc432357 的用法。为了让你看得更明白,我们编写一个经典的“生产者-消费者”场景:两个线程共享一个队列,一个负责存入数据(生产者),另一个负责取出数据(消费者)。当队列满时,生产者等待;当队列空时,消费者等待。

#### 示例 1:基础协作演示

在这个例子中,我们将模拟一个简单的场景:线程 2 (t2) 必须等待线程 1 (t1) 完成某项初始化工作后才能继续执行。

// 演示 wait() 和 notify() 基础用法的 Java 程序
class SharedResource {
    // 使用 volatile 关键字防止线程缓存,确保可见性
    // 这是一个简单的状态标志,表示“第一部分工作是否已完成”
    volatile boolean part1Done = false;

    // 同步方法 part1:由线程 t1 调用
    synchronized void part1() {
        System.out.println("-> [t1] 正在执行初始化工作...");
        // 模拟耗时工作
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        
        part1Done = true; // 标记工作已完成
        System.out.println("-> [t1] 工作完成,准备通知等待的线程...");
        
        // 通知正在该对象上等待的线程(如果有)
        // 注意:当前线程 t1 仍持有锁,直到此方法结束
        notify(); 
        System.out.println("-> [t1] 通知已发送,即将释放锁。");
    }

    // 同步方法 part2:由线程 t2 调用
    synchronized void part2() {
        // 循环检查:防止“虚假唤醒”(Spurious Wakeup)
        // 这是一个非常重要的最佳实践
        while (!part1Done) {
            try {
                System.out.println("-> [t2] 检测到条件不满足,进入等待状态...");
                // 释放锁并等待,直到被 notify() 唤醒
                wait();
                System.out.println("-> [t2] 收到通知!被唤醒并重新获取到锁。");
            }
            catch (InterruptedException e) {
                System.out.println("线程被异常中断");
            }
        }
        // 此时 part1Done 为 true,可以安全继续执行
        System.out.println("-> [t2] 检测到条件已满足,开始执行后续任务...");
    }
}

public class Main {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        // 创建线程 t1,负责执行初始化
        Thread t1 = new Thread(() -> resource.part1());

        // 创建线程 t2,依赖 t1 的结果
        Thread t2 = new Thread(() -> resource.part2());

        // 我们故意先启动 t2,演示它如何等待
        t2.start();
        
        // 稍作延迟确保 t2 先运行并进入 wait
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        t1.start();
    }
}

输出结果:

-> [t2] 检测到条件不满足,进入等待状态...
-> [t1] 正在执行初始化工作...
-> [t1] 工作完成,准备通知等待的线程...
-> [t1] 通知已发送,即将释放锁。
-> [t2] 收到通知!被唤醒并重新获取到锁。
-> [t2] 检测到条件已满足,开始执行后续任务...

从输出中我们可以看到,t2 启动后因为条件未满足而进入 INLINECODEe3c94c3f 状态,释放了锁。随后 t1 获取锁,完成工作并调用 INLINECODE6a8b2fe2。只有当 t1 执行完同步块释放锁后,t2 才真正从 wait() 返回并继续执行。

#### 示例 2:生产者与消费者(解决实际问题)

让我们看一个更复杂的例子,模拟队列的存取操作,这更接近实际开发中的场景。

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

// 这是一个简单的线程安全队列实现
class SharedBuffer {
    private Queue buffer = new LinkedList();
    private int capacity = 5; // 队列最大容量

    // 生产者方法:向队列存入数据
    public void produce(int item) throws InterruptedException {
        synchronized (this) {
            // 如果队列满了,生产者必须等待
            while (buffer.size() == capacity) {
                System.out.println("[生产者] 队列已满,等待消费者取走数据...");
                wait(); // 释放锁,等待消费者 notify
                System.out.println("[生产者] 被唤醒,再次尝试存入。");
            }
            
            buffer.add(item);
            System.out.println("[生产者] 生产了: " + item);
            
            // 通知正在等待的消费者(队列里有东西了)
            // 这里使用 notifyAll 更安全,防止唤醒了另一个生产者导致死锁
            notifyAll(); 
        }
    }

    // 消费者方法:从队列取出数据
    public void consume() throws InterruptedException {
        synchronized (this) {
            // 如果队列空了,消费者必须等待
            while (buffer.size() == 0) {
                System.out.println("[消费者] 队列为空,等待生产者放入数据...");
                wait(); // 释放锁,等待生产者 notify
                System.out.println("[消费者] 被唤醒,再次尝试取货。");
            }
            
            int item = buffer.poll();
            System.out.println("[消费者] 消费了: " + item);
            
            // 通知正在等待的生产者(队列里有空位了)
            notifyAll();
        }
    }
}

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        SharedBuffer buffer = new SharedBuffer();

        // 创建生产者线程
        Thread producerThread = new Thread(() -> {
            try {
                for (int i = 1; i  {
            try {
                for (int i = 1; i <= 10; i++) {
                    buffer.consume();
                    Thread.sleep(300); // 模拟消费慢一点
                }
            } catch (InterruptedException e) { e.printStackTrace(); }
        });

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

在这个例子中,我们使用了 INLINECODE9bc23f9f。为什么不用 INLINECODE086381d3?因为可能有两个生产者线程同时等待。如果我们使用 notify(),可能会错误地唤醒另一个生产者,而另一个生产者发现队列还是满的,会再次等待,这样就没人能唤醒消费者了,导致死锁。

常见异常与处理

在使用这些方法时,有几个陷阱是我们必须小心的:

1. 必须在同步上下文中调用

这是新手最容易犯的错误。INLINECODE74ac8d9e, INLINECODEed2b80cc, 和 INLINECODE4a4ce958 必须在当前线程持有该对象锁(即在 INLINECODE5c3d7f61 块或方法中)的情况下调用。否则,JVM 会抛出 IllegalMonitorStateException

// 错误示例
public void wrongMethod() {
    Object obj = new Object();
    obj.wait(); // 抛出 IllegalMonitorStateException,因为没持有锁
}

2. 处理 InterruptedException

INLINECODEf8ab66ab 方法会抛出 INLINECODE07943fb6。这表示线程在等待期间被其他线程中断了。作为最佳实践,我们不应该吞掉这个异常,而应该处理它,通常是通过再次设置中断标志或者向上层抛出。

3. 虚假唤醒

你可能在上面的代码中注意到了,我们在 INLINECODE7f669190 循环中调用 INLINECODE13be43cc,而不是用 if 语句。

// 推荐做法
while (conditionNotMet) {
    wait();
}

这是为什么呢?因为操作系统底层允许所谓的“虚假唤醒”,即线程在没有收到 INLINECODEf9c39b46 的情况下也能从 INLINECODE1e897473 返回。如果不放在循环里再次检查条件,程序可能会在条件未真正满足时继续向下执行,导致难以排查的逻辑错误。

性能优化与最佳实践

在实际的高并发开发中,直接使用 INLINECODE10481ea9 和 INLINECODE8ccf279b 往往比较繁琐且容易出错。我们可以考虑以下优化方向:

  • 优先使用并发工具包:在 Java 5+ 中,INLINECODEdb0cc539 包提供了更高级的工具,如 INLINECODE7c195e45(上面示例的完美替代者)、INLINECODEdd6be4eb、INLINECODE6bfe36cb 等。它们封装了 wait/notify 逻辑,使用起来更安全、更直观。
  • 减少锁的粒度synchronized 块的范围应尽可能小。例如,在获取数据之前做不需要锁的准备工作,只在必要时才进入同步块。
  • notify() vs notifyAll():默认情况下,如果你的等待线程有多个不同类型(比如消费者和生产者都在等),或者逻辑比较复杂,使用 INLINECODEd00d24fb 更安全。只有在确定只有一个等待线程或者所有等待线程都处理相同任务时,才使用 INLINECODEa3343f90 来减少上下文切换的开销。

总结

在这篇文章中,我们深入探讨了 Java 中线程间通信的核心机制 —— INLINECODE7db304ab 和 INLINECODE942c4aa7。我们不仅了解了它们的基本用法,还通过代码示例看到了它们在实际场景(如生产者-消费者模型)中的应用,以及如何正确处理异常和“虚假唤醒”问题。

掌握这些底层的同步机制对于编写高性能、线程安全的多线程 Java 程序至关重要。虽然在实际业务代码中我们可能会更多地使用 INLINECODE6241e240 或 INLINECODEeab8d6d4 等高级封装,但理解其背后的原理,能让你在面对复杂的并发问题时游刃有余。

希望这篇文章能帮助你彻底搞懂 Java 线程通信的奥秘!下次当你遇到多线程协作的问题时,不妨试着画个图,理清楚谁在等、谁在叫,你会发现它其实并不复杂。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/22917.html
点赞
0.00 平均评分 (0% 分数) - 0