Java Object notifyAll() 方法(手把手讲解)

更新时间:

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

欢迎加入小哈的星球 ,你将获得:专属的项目实战(已更新的所有项目都能学习) / 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 notifyAll() 方法作为 Java 线程同步机制的重要组成部分,常用于唤醒所有因调用 wait() 方法而处于等待状态的线程。然而,对于编程初学者和中级开发者而言,理解其原理和正确使用方式并非易事。本文将通过循序渐进的方式,结合生活化的比喻和代码示例,深入讲解 notifyAll() 的工作机制、使用场景及常见误区,帮助读者掌握这一关键知识点。


线程同步的基础:wait()、notify() 与 notifyAll()

在探讨 notifyAll() 之前,我们需要先了解 Java 线程同步的三个核心方法:wait()notify()notifyAll()。它们均定义在 Object 类中,因此所有 Java 对象都具备这些方法。

1. wait() 方法:线程的“暂停”操作

wait() 方法用于让当前线程暂停执行,并释放其持有的对象锁。调用该方法的线程会被放入与对象关联的等待队列中,直到被其他线程唤醒。
注意

  • wait() 必须在同步代码块(synchronized)中调用,否则会抛出 IllegalMonitorStateException
  • 调用 wait() 后,线程会释放锁,但其他线程需重新获取锁才能继续执行。

示例代码

public class WaitExample {  
    private final Object lock = new Object();  

    public void doWait() {  
        synchronized (lock) {  
            System.out.println("线程进入等待状态");  
            try {  
                lock.wait(); // 暂停线程并释放锁  
            } catch (InterruptedException e) {  
                Thread.currentThread().interrupt();  
            }  
            System.out.println("线程被唤醒");  
        }  
    }  
}

2. notify() 和 notifyAll():线程的“唤醒”操作

  • notify():随机选择等待队列中的一个线程,将其移至锁的等待队列(ready queue),等待重新获取锁。
  • notifyAll():唤醒所有因 wait() 而等待的线程,将它们全部移至锁的等待队列。

关键区别
| 方法 | 唤醒线程数量 | 使用场景示例 |
|---------------|--------------|--------------------------|
| notify() | 单个线程 | 生产者-消费者模式(单生产者/单消费者) |
| notifyAll() | 所有线程 | 多线程竞争共享资源的场景(如多个消费者) |

比喻解释
可以将 wait()notify()notifyAll() 比作交通灯的控制:

  • wait() 是线程的“红灯”,强制暂停;
  • notify() 是为某一条车道的车辆亮“绿灯”;
  • notifyAll() 则是让所有车道同时亮“绿灯”。

notifyAll() 的工作原理与使用场景

1. 原理剖析:唤醒所有等待线程

当调用 notifyAll() 时,所有因 wait() 而暂停的线程会被放入锁的等待队列,但它们需要重新竞争锁才能继续执行。因此,即使被唤醒,线程仍需通过竞争锁才能真正运行。

流程图示意

线程 A 调用 lock.wait() → 进入等待队列 → 释放锁  
线程 B 调用 lock.notifyAll() → 唤醒所有等待线程 → 线程 A 等进入 ready 队列  
线程 B 释放锁后 → 线程 A 等竞争锁 → 获得锁后继续执行  

2. 典型使用场景:多消费者模式

在生产者-消费者问题中,若存在多个消费者线程等待资源,则 notifyAll() 能确保所有消费者有机会竞争资源。

示例:多消费者队列

public class MultiConsumerExample {  
    private final Object lock = new Object();  
    private List<String> queue = new ArrayList<>();  

    public void produce(String item) {  
        synchronized (lock) {  
            queue.add(item);  
            System.out.println("生产了:" + item);  
            lock.notifyAll(); // 唤醒所有消费者  
        }  
    }  

    public String consume() {  
        synchronized (lock) {  
            while (queue.isEmpty()) {  
                try {  
                    lock.wait(); // 无资源时等待  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                    return null;  
                }  
            }  
            String item = queue.remove(0);  
            System.out.println("消费了:" + item);  
            return item;  
        }  
    }  
}

分析

  • 生产者调用 produce() 时,通过 notifyAll() 唤醒所有消费者,避免因 notify() 的随机性导致某些消费者未被唤醒。
  • 消费者在 consume() 中循环检查队列是否为空,确保在资源未就绪时持续等待。

notifyAll() 与 notify() 的对比:何时选择谁?

1. 选择 notify() 的情况

当多个线程等待资源时,若只需唤醒一个线程即可满足需求,使用 notify() 可避免不必要的线程竞争。例如:

// 单生产者-单消费者场景  
public void produce() {  
    synchronized (lock) {  
        // 生产资源后通知一个消费者  
        lock.notify(); // 仅需唤醒一个消费者  
    }  
}  

2. 选择 notifyAll() 的情况

在以下场景中,必须使用 notifyAll()

  • 多线程竞争资源:如多个消费者等待同一个资源池。
  • 条件逻辑复杂:例如,当多个线程因不同条件等待时,无法确定哪个线程需要被唤醒。

错误示例(未使用 notifyAll() 的问题)

// 错误代码:多个消费者可能未被唤醒  
public void produce() {  
    synchronized (lock) {  
        queue.add("item");  
        lock.notify(); // 可能遗漏其他消费者  
    }  
}  

常见误区与注意事项

1. 必须在同步块中调用

若在非 synchronized 块中调用 wait()notify()notifyAll(),会抛出 IllegalMonitorStateException。例如:

// 错误示例:直接调用 lock.wait()  
public void wrongWay() {  
    Object lock = new Object();  
    lock.wait(); // 抛出异常!  
}  

2. 避免无限等待

若线程因条件未满足而无限等待,需通过超时机制或外部中断处理。例如:

lock.wait(1000); // 等待最多1秒  

3. 唤醒后需重新检查条件

由于唤醒线程可能因竞争失败而未获取锁,因此在 wait() 后应始终使用循环检查条件。例如:

while (queue.isEmpty()) {  
    lock.wait();  
}  

实战案例:使用 notifyAll() 实现线程池

场景:创建一个简单的线程池,当任务队列为空时,工作线程进入等待状态,任务提交时唤醒所有线程竞争执行。

public class SimpleThreadPool {  
    private final Object lock = new Object();  
    private final List<Runnable> tasks = new ArrayList<>();  
    private final List<Worker> workers;  

    public SimpleThreadPool(int size) {  
        workers = new ArrayList<>();  
        for (int i = 0; i < size; i++) {  
            Worker worker = new Worker();  
            worker.start();  
            workers.add(worker);  
        }  
    }  

    public void execute(Runnable task) {  
        synchronized (lock) {  
            tasks.add(task);  
            lock.notifyAll(); // 唤醒所有工作线程  
        }  
    }  

    private class Worker extends Thread {  
        public void run() {  
            while (true) {  
                Runnable task = null;  
                synchronized (lock) {  
                    while (tasks.isEmpty()) {  
                        try {  
                            lock.wait(); // 无任务时等待  
                        } catch (InterruptedException e) {  
                            return;  
                        }  
                    }  
                    task = tasks.remove(0);  
                }  
                task.run();  
            }  
        }  
    }  
}

分析

  • 提交任务时,通过 notifyAll() 唤醒所有工作线程,避免因随机唤醒导致部分线程闲置。
  • 每个工作线程在 wait() 后需重新检查任务队列,确保条件满足后再执行。

结论

Java Object notifyAll() 方法是协调多线程通信的重要工具,其核心作用是唤醒所有因 wait() 暂停的线程。通过理解其工作原理、合理选择 notify()notifyAll(),以及规避常见陷阱,开发者可以编写出高效且稳定的并发程序。

在实际开发中,建议优先使用更高层次的并发工具类(如 java.util.concurrent 包中的 BlockingQueueExecutorService),但掌握底层 wait/notify 机制仍对理解并发编程的本质大有帮助。希望本文能帮助读者在多线程开发中更加得心应手!

最新发布