Java Object对象wait()、notify()、notifyAll()函数详解与项目实战

在Java并发编程中,wait(), notify(), 和 notifyAll()是用于实现线程间精确协作的核心机制。它们允许线程在特定条件未满足时主动等待,并在条件可能满足时被唤醒,从而高效地协调工作。下面这张表格帮助你快速掌握它们的特点。

特性 wait() notify() notifyAll()
所属类 Object Object Object
作用 让当前线程释放锁并进入等待 唤醒一个在该对象上等待的线程 唤醒所有在该对象上等待的线程
锁处理 释放当前持有的对象锁 不释放锁,需等同步块执行完毕 不释放锁,需等同步块执行完毕
调用前提 必须在同步代码块(synchronized)​​ 内调用 必须在同步代码块(synchronized)​​ 内调用 必须在同步代码块(synchronized)​​ 内调用
线程状态 进入 WAITING 或 TIMED_WAITING 被唤醒的线程进入 BLOCKED,竞争锁 所有被唤醒的线程进入 BLOCKED,竞争锁

🔍 理解核心机制

要正确使用这三个方法,需要理解其背后的几个关键点。

  • 与对象锁绑定 ​:这三个方法的调用都依赖于对象的内置锁(监视器锁)​ 。线程在调用它们之前,​必须 先获得该对象的锁,这就是为什么它们必须在 synchronized代码块或方法内使用,否则会抛出 IllegalMonitorStateException

  • 等待与唤醒的条件检查 ​:一个至关重要的实践是,线程被唤醒后,​不应该立即假定其等待的条件已经满足。这是因为:

    • 虚假唤醒 ​:在某些情况下,即使没有线程调用 notify,等待的线程也可能被唤醒。

    • 通知的泛化 ​:notify()是随机唤醒一个线程,它可能并非在等待当前已满足的条件。

      因此,线程被唤醒后,必须使用循环重新检查等待条件。如果条件不满足,应继续等待 。

    scss 复制代码
    synchronized (sharedLock) {
        while (!conditionIsMet) { // 必须用循环检查条件
            sharedLock.wait();
        }
        // ... 条件满足后执行任务
    }
  • 为什么定义在Object类中 ​:这是因为这些方法操作的是对象的同步锁 。每个对象都有一把锁,wait()方法会让当前线程释放它正持有的这个对象 的锁,而 notify()则是唤醒其他正在等待这个对象 的锁的线程。锁是对象级别的,所以这些方法自然定义在 Object类中,而非 Thread类 。

⚖️ wait() 与 sleep() 的抉择

Thread.sleep()也能让线程暂停,但它与 wait()有本质区别,具体对比如下 :

特性 wait() sleep()
所属类 Object Thread
锁的行为 释放已持有的对象锁 不释放任何锁
使用场景 线程间协作,等待特定条件 单纯暂停当前线程的执行
唤醒方式 需被 notify/notifyAll唤醒或超时 在指定时间后自动恢复
调用要求 必须在 synchronized块内 可以在任何地方调用

简单来说,当需要让线程等待某个条件并允许其他线程获取锁时,用 wait();当只是想暂时让线程休眠片刻,且不希望影响其他线程获取锁时,用 sleep()

🛠️ 项目实战:生产者-消费者模型

生产者-消费者问题是一个经典的多线程协作案例。假设有一个共享的、容量有限的仓库,生产者负责向仓库添加产品,消费者负责从仓库取走产品。规则是:仓库满时生产者等待,仓库空时消费者等待 。

以下是使用 wait()notifyAll()实现的核心代码:

java 复制代码
import java.util.LinkedList;

public class Warehouse {
    private final int MAX_SIZE = 10; // 仓库最大容量
    private final LinkedList<Object> items = new LinkedList<>(); // 存储产品的链表

    // 生产方法
    public synchronized void produce(Object product) throws InterruptedException {
        while (items.size() >= MAX_SIZE) {
            // 仓库已满,生产者线程等待
            wait();
        }
        items.add(product); // 生产产品
        System.out.println("生产了一个产品,当前库存: " + items.size());
        notifyAll(); // 通知可能正在等待的消费者(或其它生产者)
    }

    // 消费方法
    public synchronized Object consume() throws InterruptedException {
        while (items.isEmpty()) {
            // 仓库为空,消费者线程等待
            wait();
        }
        Object product = items.removeFirst(); // 消费产品
        System.out.println("消费了一个产品,当前库存: " + items.size());
        notifyAll(); // 通知可能正在等待的生产者(或其它消费者)
        return product;
    }
}

代码要点解析​ :

  • 条件循环检查 :在 produceconsume方法中,都使用了 while循环来判断条件(库存已满或为空),而不是 if语句。这是为了防止虚假唤醒,确保线程被唤醒后条件确实满足才继续执行。
  • 使用 notifyAll() :这里选择使用 notifyAll()是因为有生产者和消费者两类线程在等待。如果使用 notify(),可能会错误地只唤醒另一个生产者(当仓库已满时),而真正需要被唤醒的消费者则可能一直得不到执行,导致死锁notifyAll()会唤醒所有等待的线程,让它们去竞争锁并重新检查条件,从而更安全 。

在 Spring Boot 项目中,除了经典的生产者-消费者模式,wait()notify()/notifyAll()机制还可以应用于多种需要精细线程协作的场景。下面通过一个表格汇总一些典型的使用案例。

应用场景 核心目标 关键协作机制
资源池管理​ (如数据库连接池) 控制资源的使用,当资源耗尽时,新请求等待;资源释放时,通知等待线程。 获取资源时若无可用量,则 wait();释放资源时,调用 notifyAll()唤醒等待线程。
异步任务结果同步 主线程提交异步任务后,等待工作线程处理完毕并返回结果。 主线程提交任务后 wait();工作线程完成后 notify()notifyAll()并设置结果。
自定义定时任务调度 实现一个调度器,让任务在特定时间点执行或按固定间隔周期性执行。 调度线程计算下一个执行时间点并 wait(timeout);任务添加或取消时 notify()重新计算。
系统初始化屏障 确保多个核心服务完成初始化后,应用才正式对外提供服务。 启动线程在各服务初始化前 wait();每个服务初始化完成后检查并调用 notifyAll()

🛠️ Spring Boot 项目实战案例

以下是一个简化的 ​异步任务结果同步​ 示例,模拟在 Spring Boot 中处理一个耗时操作。

typescript 复制代码
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class AsyncTaskProcessor {
    // 用于存储任务结果和关联的锁对象
    private final ConcurrentHashMap<String, TaskResult> taskResults = new ConcurrentHashMap<>();

    // 提交异步任务的方法
    public void submitTask(String taskId, Runnable task) {
        // 初始化任务结果和锁对象
        taskResults.put(taskId, new TaskResult());
        // 启动异步线程执行任务
        new Thread(() -> {
            // 模拟耗时操作
            try {
                Thread.sleep(2000);
                // 任务完成,设置结果并通知
                synchronized (this) {
                    TaskResult result = taskResults.get(taskId);
                    result.setData("Task " + taskId + " completed!");
                    result.setDone(true);
                    this.notifyAll(); // 通知所有等待此任务结果的线程
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }).start();
    }

    // 获取任务结果的方法(阻塞直到任务完成)
    public String getTaskResult(String taskId) throws InterruptedException {
        TaskResult result = taskResults.get(taskId);
        if (result == null) {
            throw new IllegalArgumentException("Task ID not found: " + taskId);
        }
        synchronized (this) {
            while (!result.isDone()) {
                this.wait(); // 等待任务完成的通知
            }
            return result.getData();
        }
    }

    // 内部类,封装任务结果
    private static class TaskResult {
        private String data;
        private boolean isDone = false;

        // Getter and Setter ...
        public String getData() { return data; }
        public void setData(String data) { this.data = data; }
        public boolean isDone() { return isDone; }
        public void setDone(boolean done) { isDone = done; }
    }
}

代码要点解析​:

  • 异步执行submitTask方法会立即返回,不会阻塞调用线程,耗时任务在后台执行。
  • 结果等待getTaskResult方法会检查任务是否完成。如果未完成,调用线程会在 wait()处阻塞。
  • 协作机制 :当后台任务线程执行完毕,会设置结果标记,并调用 notifyAll()唤醒所有正在等待该任务结果的线程。被唤醒的线程会再次检查 isDone条件,如果为真则获取结果。
  • 锁对象 :此例中使用 this(即 AsyncTaskProcessor实例本身)作为锁对象,确保 waitnotifyAll作用于同一个监视器。

⚠️ 重要注意事项与最佳实践

  1. 循环条件检查 :务必在 while循环中检查等待条件,而不是 if语句,以防止虚假唤醒(spurious wakeup)。
  2. 优先考虑 notifyAll() :除非你能百分之百确定只有一个线程在等待,且唤醒它总是正确的,否则优先使用 notifyAll()更为安全,可以避免某些线程被永久遗忘(线程饥饿)。
  3. 考虑使用 Java.util.concurrent 工具包 :对于复杂的生产-消费场景或线程协作,Java 5 引入的 java.util.concurrent包提供了更高级、更安全的工具,如 BlockingQueue(阻塞队列)。使用 ArrayBlockingQueue可以轻松实现生产者-消费者模式,而无需手动处理 wait/notify的底层细节,大大简化了代码并降低了出错风险 。
  4. 与 Spring 框架的集成 :在 Spring Boot 中,可以利用 @Async注解、事件监听机制或消息中间件(如 RabbitMQ, Kafka)来实现更解耦、更易于管理的异步处理和线程间通信,这往往是比直接使用底层 wait/notify更符合 Spring 设计哲学的做法。

💎 总结

wait(), notify(), 和 notifyAll()是 Java 底层线程协作的强大工具。理解其机制并遵循"循环检查条件 "和"谨慎选择通知方式 "的原则,是编写正确、健壮的多线程程序的关键。对于现代并发开发,积极了解并使用 java.util.concurrent包中的高级组件(如 Lock, Condition, 各种阻塞队列等)通常是更优的选择,它们提供了更好的灵活性和可控性。

在 Spring Boot 项目中,wait()notify()/notifyAll()的用武之地在于那些需要精细控制线程等待与唤醒的场景。虽然实际开发中我们更倾向于使用更高级的并发工具或框架特性,但理解这些底层机制的原理,对于设计健壮的并发模块、排查复杂的线上问题依然至关重要。

相关推荐
Moment4 小时前
Node.js v25.0.0 发布——性能、Web 标准与安全性全面升级 🚀🚀🚀
前端·javascript·后端
IT_陈寒4 小时前
Vite 3.0 性能优化实战:5个技巧让你的构建速度提升200% 🚀
前端·人工智能·后端
程序新视界4 小时前
MySQL的整体架构及功能详解
数据库·后端·mysql
绝无仅有4 小时前
猿辅导Java面试真实经历与深度总结(二)
后端·面试·github
绝无仅有4 小时前
猿辅导Java面试真实经历与深度总结(一)
后端·面试·github
Victor3565 小时前
Redis(76)Redis作为缓存的常见使用场景有哪些?
后端
Victor3565 小时前
Redis(77)Redis缓存的优点和缺点是什么?
后端
摇滚侠8 小时前
Spring Boot 3零基础教程,WEB 开发 静态资源默认配置 笔记27
spring boot·笔记·后端
天若有情67311 小时前
Java Swing 实战:从零打造经典黄金矿工游戏
java·后端·游戏·黄金矿工·swin