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
包中的 BlockingQueue
或 ExecutorService
),但掌握底层 wait/notify
机制仍对理解并发编程的本质大有帮助。希望本文能帮助读者在多线程开发中更加得心应手!