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

相关推荐
你的人类朋友2 小时前
✍️记录自己的git分支管理实践
前端·git·后端
像风一样自由20202 小时前
Go语言入门指南-从零开始的奇妙之旅
开发语言·后端·golang
合作小小程序员小小店3 小时前
web网页开发,在线考勤管理系统,基于Idea,html,css,vue,java,springboot,mysql
java·前端·vue.js·后端·intellij-idea·springboot
间彧4 小时前
SpringBoot + MyBatis-Plus + Dynamic-Datasource 读写分离完整指南
数据库·后端
间彧4 小时前
数据库读写分离下如何解决主从同步延迟问题
后端
码事漫谈4 小时前
C++中的线程同步机制浅析
后端
间彧4 小时前
在高并发场景下,动态数据源切换与Seata全局事务锁管理如何协同避免性能瓶颈?
后端
码事漫谈4 小时前
CI/CD集成工程师前景分析:与开发岗位的全面对比
后端
间彧4 小时前
在微服务架构下,如何结合Spring Cloud实现动态数据源的路由管理?
后端
间彧5 小时前
动态数据源切换与Seata分布式事务如何协同工作?
后端