Java Object notify() 方法(保姆级教程)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目:《Spring AI 项目实战》 正在持续爆肝中,基于 Spring AI + Spring Boot 3.x + JDK 21..., 点击查看 ;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

在多线程编程中,线程之间的协作与通信是核心挑战之一。Java 提供了 Object 类的 wait()notify()notifyAll() 方法,这些方法与 synchronized 关键字结合使用,能够实现线程间的高效协作。其中,notify() 方法作为通知机制的核心,常常让开发者感到困惑。本文将从基础概念出发,通过案例和类比,深入解析 Java Object notify() 方法的原理与用法,帮助开发者理解其在多线程环境中的实际应用场景。


一、线程同步基础:对象锁与 wait() 方法

1.1 对象锁(Monitor)的核心作用

Java 中的 synchronized 关键字通过对象锁(Monitor)实现线程同步。每个对象都拥有一个与生俱来的锁,当线程进入 synchronized 方法或代码块时,必须先获取该对象的锁。例如:

public synchronized void process() {
    // 只有一个线程能执行这段代码
}

这个机制确保了同一时间只有一个线程可以修改共享数据,避免了竞争条件(Race Condition)。

1.2 wait() 方法:让线程进入等待状态

当线程需要等待某个条件满足时,可以通过 wait() 方法将自己放入对象的等待队列中,并释放当前锁。例如:

public synchronized void await() {
    while (!condition) {
        wait(); // 进入等待队列,释放锁
    }
    // 条件满足后继续执行
}

此时线程会暂停执行,直到被其他线程通过 notify()notifyAll() 唤醒。


二、notify() 方法的运作原理与使用场景

2.1 notify() 的核心功能

notify() 方法用于唤醒在对象等待队列中等待时间最长的线程(非随机选择)。例如:

public synchronized void signal() {
    // 修改共享数据后通知等待线程
    notify(); // 唤醒一个等待线程
}

需要注意的是,被唤醒的线程并不会立即执行,而是需要重新竞争锁,才能继续执行后续代码。

2.2 形象比喻:厨师与顾客的协作

想象一个餐厅场景:

  • 厨师(Producer):准备食物后,通过 notify() 通知“等待队列”中的第一个顾客(Consumer)。
  • 顾客(Consumer):在食物未准备好时,通过 wait() 释放座位(锁),进入等待队列。

此时,notify() 就像服务员喊一声“您的餐点准备好了”,唤醒一个顾客来取餐,但顾客需要重新排队拿号(竞争锁)才能继续用餐。


三、notify()notifyAll() 的区别与选择

3.1 notifyAll() 的唤醒逻辑

notifyAll() 会唤醒等待队列中的 所有线程,而 notify() 只唤醒一个。例如:

public synchronized void signalAll() {
    notifyAll(); // 唤醒所有等待线程
}

关键区别
| 方法 | 唤醒线程数量 | 适用场景 |
|---------------|--------------|----------------------------|
| notify() | 单个线程 | 只需唤醒一个线程时(如单生产者单消费者) |
| notifyAll() | 所有线程 | 需要唤醒多个线程时(如多个消费者等待同一资源) |

3.2 为什么 notifyAll() 更安全?

假设一个资源被多个线程竞争,若仅使用 notify(),可能因线程调度顺序导致某些线程永远无法被唤醒。例如,两个消费者线程 A 和 B:

  • 生产者调用 notify(),可能只唤醒 A,而 B 仍处于等待状态。
  • 若 A 的优先级较低,B 可能因无法竞争到锁而一直等待。

此时 notifyAll() 能确保所有线程都有机会竞争锁,避免死锁。


四、经典案例:生产者-消费者问题

4.1 问题描述

生产者线程生产数据,消费者线程消费数据。要求:

  • 当缓冲区已满时,生产者等待;
  • 当缓冲区为空时,消费者等待。

4.2 代码实现与 notify() 应用

public class ProducerConsumerExample {
    private final Object lock = new Object();
    private int buffer = 0;
    private static final int CAPACITY = 10;

    // 生产者线程
    public void produce() {
        synchronized (lock) {
            while (buffer >= CAPACITY) {
                try {
                    lock.wait(); // 缓冲区满时等待
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            buffer++;
            System.out.println("Produced: " + buffer);
            lock.notifyAll(); // 通知消费者
        }
    }

    // 消费者线程
    public void consume() {
        synchronized (lock) {
            while (buffer <= 0) {
                try {
                    lock.wait(); // 缓冲区空时等待
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            buffer--;
            System.out.println("Consumed: " + buffer);
            lock.notify(); // 通知生产者
        }
    }
}

关键点解析

  • 生产者在 produce() 中,当缓冲区满时调用 wait(),释放锁并等待;
  • 消费后调用 notifyAll(),确保所有等待的生产者/消费者有机会被唤醒;
  • notify() 在消费后仅唤醒一个线程,但 notifyAll() 更保险(因可能有多个生产者/消费者线程)。

五、常见误区与最佳实践

5.1 误区一:未在同步代码块中调用 wait()/notify()

若在非 synchronized 区域调用这些方法,会抛出 IllegalMonitorStateException。例如:

// 错误示例:未加锁直接调用
public void wrongUsage() {
    lock.notify(); // 抛出异常!
}

正确做法:必须在 synchronized 方法或代码块中调用。

5.2 误区二:忽略循环检查条件

直接使用 if 条件判断可能导致“虚假唤醒”(Spurious Wakeup),应改用 while 循环:

// 错误示例:可能因线程调度导致条件不满足
if (buffer == 0) {
    wait();
}

// 正确示例:循环检查条件
while (buffer == 0) {
    wait();
}

5.3 最佳实践建议

  1. 始终在 synchronized 区域中调用 wait()notify()
  2. 优先使用 Condition 接口(Java 5+ java.util.concurrent.locks 包),提供更灵活的条件控制;
  3. 避免过度依赖 notify(),考虑使用更高层的并发工具类(如 BlockingQueue)。

六、进阶扩展:与 Condition 接口的对比

6.1 Condition 的优势

Condition 接口(通过 newCondition() 方法创建)提供了更细粒度的控制,例如:

Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

public void produce() {
    lock.lock();
    try {
        while (buffer >= CAPACITY) {
            notFull.await();
        }
        // 生产操作...
        notEmpty.signal(); // 仅通知相关条件
    } finally {
        lock.unlock();
    }
}

对比总结
| 特性 | Object 方法 | Condition 接口 |
|--------------------|------------------------|--------------------------|
| 灵活性 | 固定绑定对象锁 | 支持多条件管理 |
| 异常处理 | 需手动处理中断 | 自动恢复中断状态 |
| 推荐场景 | 简单场景或兼容旧代码 | 复杂条件逻辑的高并发场景 |


结论:合理使用 notify() 方法提升多线程协作效率

Java Object notify() 方法是实现线程间协作的核心工具之一,但其正确使用需要开发者对线程状态转换、锁机制和条件判断有深刻理解。通过本文的案例分析与误区解析,开发者可以:

  1. 掌握 wait()notify() 的基本语法与协作流程;
  2. 理解 notify()notifyAll() 的适用场景;
  3. 避免常见错误,确保代码的健壮性与可维护性。

在实际开发中,建议优先使用更高层的并发工具类(如 BlockingQueueCompletableFuture),但在需要精细控制线程行为时,熟练运用 Object 的等待/通知机制仍是开发者不可或缺的技能。

最新发布