【并发编程实战】6、Java等待-通知机制实战:从循环等待优化到线程高效协作
本文介绍了Java并发编程中“循环等待”的弊端及优化方案。循环等待会导致CPU空转、响应延迟和可扩展性差,而“等待-通知”机制通过wait()/notify()/notifyAll()实现线程高效协作。文章通过就医流程类比解释了等待-通知机制的四要素,并详细解析了其核心原理和交互流程。优化后的资源分配器和转账场景代码展示了如何避免循环等待,同时强调了关键实现要点,如必须使用while循环检查条件、

在Java并发编程中,循环等待是导致CPU空转、性能损耗的“隐形杀手”,而“等待-通知”机制(wait()/notify()/notifyAll())是解决这一问题的核心方案。
本文从循环等待的痛点切入,通过“原理类比+完整代码+可视化图示”,彻底讲清等待-通知机制的实现逻辑、避坑要点与实战场景,助力开发者实现线程高效协作,同时适配搜索引擎关键词(如“Java等待-通知机制”“循环等待优化”“线程协作最佳实践”),便于快速检索。
引言:并发编程的“循环等待”困境
当多线程竞争资源时,传统的“循环等待”通过死循环反复检查条件(如资源是否可用),会导致线程在等待期间持续占用CPU,造成资源浪费。这一问题在高并发场景下会急剧放大,甚至引发服务响应延迟。
一、循环等待的“坑”:为什么必须优化?
1. 传统循环等待的错误实现(CPU空转示例)
两个文档均以“资源申请”场景为例,展示循环等待的低效性:
// ❌ 错误示例:循环等待浪费CPU
class Allocator {
private List<Object> als = new ArrayList<>();
// 申请资源
synchronized boolean apply(Object from, Object to) {
if (!als.contains(from) && !als.contains(to)) {
als.add(from);
als.add(to);
return true;
}
return false;
}
}
class Account {
private Allocator actr;
private int balance;
// 转账时循环申请资源
void transfer(Account target, int amt) {
// 空循环反复检查,CPU持续空转
while (!actr.apply(this, target)) {
// 无任何休眠,CPU占用率高
}
try {
// 执行转账逻辑
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
} finally {
actr.free(this, target);
}
}
}
2. 循环等待的三大核心问题(图示解析)
graph LR
A[线程启动] --> B[获取锁检查条件]
B -->|条件不满足| C[释放锁]
C --> D[立即重新循环]
D --> B[再次获取锁检查条件]
note over C,D: 无休眠,CPU空转
style D fill:#ffcccc,color:#ff0000
- CPU资源浪费:线程在循环中反复“获取锁-检查-释放锁”,无实际业务操作却占用CPU;
- 响应延迟:即使条件满足,线程也需等待下一次循环检查才能感知,无法即时响应;
- 可扩展性差:大量线程同时循环等待时,锁竞争加剧,系统性能急剧下降。
二、等待-通知机制:原理与现实类比
等待-通知机制的核心是:条件不满足时线程释放锁进入等待状态(不消耗CPU),条件满足时被主动唤醒,重新竞争锁执行。其逻辑可通过“就医流程”类比,直观理解:
1. 现实世界类比:就医流程(四要素对应)
| 就医流程 | 等待-通知机制四要素 | 核心作用 |
|---|---|---|
| 挂号分诊(获取就诊资格) | 互斥锁(synchronized锁定) | 保护临界区,避免多线程并发冲突 |
| 等待叫号(诊室空闲) | 条件谓词(如“资源未被占用”) | 线程等待的具体条件 |
| 未叫号时坐候(不打扰) | 等待时机(条件不满足时wait()) | 避免无效循环,释放CPU |
| 叫号通知(诊室空闲) | 通知时机(条件满足时notifyAll()) | 即时唤醒等待线程,提高响应性 |
2. 等待-通知机制核心原理(线程交互流程)
等待-通知机制分为“等待方”和“通知方”两大角色,其交互流程如下(序列图):
sequenceDiagram
participant 等待方线程A
participant 通知方线程B
participant 锁对象(互斥锁)
participant 条件(如“资源可用”)
等待方线程A->>锁对象(互斥锁): 1. 获取锁
等待方线程A->>条件(如“资源可用”): 2. 检查条件(不满足)
等待方线程A->>锁对象(互斥锁): 3. 调用wait(),释放锁并进入等待队列
等待方线程A-->>等待方线程A: 4. 线程A进入WAITING状态(不消耗CPU)
通知方线程B->>锁对象(互斥锁): 5. 获取锁(线程A释放后)
通知方线程B->>条件(如“资源可用”): 6. 修改条件(如释放资源,条件满足)
通知方线程B->>锁对象(互斥锁): 7. 调用notifyAll(),唤醒所有等待线程
通知方线程B->>锁对象(互斥锁): 8. 释放锁
等待方线程A->>锁对象(互斥锁): 9. 被唤醒后重新竞争锁(成功获取)
等待方线程A->>条件(如“资源可用”): 10. 再次检查条件(满足)
等待方线程A->>等待方线程A: 11. 执行临界区业务逻辑
三、核心实现:synchronized + wait()/notifyAll()
基于等待-通知机制的核心原理,我们对“资源分配器”和“转账场景”进行优化,彻底解决循环等待问题。
1. 优化后的资源分配器(关键代码)
class Allocator {
// 管理已分配的资源(如账户对象)
private List<Object> allocatedResources = new ArrayList<>();
// 申请资源:条件不满足时wait()
public synchronized void apply(Object from, Object to) throws InterruptedException {
// ✅ 经典范式:while循环检查条件(避免虚假唤醒/条件变化)
while (allocatedResources.contains(from) || allocatedResources.contains(to)) {
System.out.println("资源被占用,线程进入等待状态");
this.wait(); // 释放锁,进入等待队列
}
// 条件满足:分配资源
allocatedResources.add(from);
allocatedResources.add(to);
System.out.println("资源申请成功");
}
// 释放资源:条件满足时notifyAll()
public synchronized void free(Object from, Object to) {
allocatedResources.remove(from);
allocatedResources.remove(to);
System.out.println("资源释放,通知所有等待线程");
this.notifyAll(); // 唤醒所有等待线程(避免线程饥饿)
}
}
2. 转账场景优化(基于Allocator)
class Account {
private Allocator resourceAllocator = new Allocator(); // 资源分配器
private int id; // 账户唯一ID(可选,配合锁顺序优化)
private int balance;
public void transfer(Account target, int amount) throws InterruptedException {
// 1. 申请资源(转出账户+转入账户)
resourceAllocator.apply(this, target);
try {
// 2. 执行转账业务(临界区)
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
System.out.printf("转账成功:从账户%d转出%d到账户%d,余额:%d%n",
this.id, amount, target.id, this.balance);
}
} finally {
// 3. 释放资源(必须在finally中,避免异常导致资源泄漏)
resourceAllocator.free(this, target);
}
}
// Getter/Setter省略
}
四、关键辨析:避免踩坑的3个核心对比
等待-通知机制的实现中,“while vs if”“notify vs notifyAll()”“wait() vs sleep()”是最易踩坑的点,必须明确差异。
1. while循环 vs if判断:为什么必须用while?
// ✅ 正确:while循环(重新检查条件)
while (!condition) {
lock.wait();
}
// ❌ 错误:if判断(跳过重新检查)
if (!condition) {
lock.wait();
}
核心原因(图示风险):
- 虚假唤醒:线程可能在无通知的情况下被唤醒(JVM底层机制);
- 条件变化:多个等待线程被唤醒后,条件可能被先执行的线程修改。
graph LR
A[线程A被虚假唤醒] --> B[if判断:跳过条件检查]
B --> C[执行临界区逻辑]
note over B,C: 条件实际不满足,导致错误
style C fill:#ffcccc,color:#ff0000
D[线程B被唤醒] --> E[while循环:重新检查条件]
E -->|条件不满足| F[再次wait()]
E -->|条件满足| G[执行临界区逻辑]
style G fill:#ccffcc,color:#008000
2. notify() vs notifyAll():为什么优先用notifyAll()?
// ✅ 推荐:notifyAll()(唤醒所有等待线程)
lock.notifyAll();
// ❌ 谨慎:notify()(随机唤醒一个线程,可能导致饥饿)
lock.notify();
notify()的风险场景(图示):
graph LR
锁对象-->|持有资源AB| 线程1
锁对象-->|持有资源CD| 线程2
等待队列-->线程3[等待AB]
等待队列-->线程4[等待CD]
线程1->>锁对象: 释放AB,调用notify()
锁对象->>线程4: 随机唤醒线程4
线程4->>锁对象: 检查CD(仍被线程2持有)
线程4->>等待队列: 重新wait()
等待队列-->线程3[线程3永远未被唤醒,饥饿]
style 线程3 fill:#ffcccc,color:#ff0000
结论:除非明确只有一个线程等待,否则必须用notifyAll(),避免线程饥饿。
3. wait() vs sleep():核心差异对比
| 特性 | wait() | sleep() |
|---|---|---|
| 锁释放 | ✅ 释放持有锁(必须在synchronized内) | ❌ 不释放锁(任何场景可调用) |
| 所属类 | Object类(锁对象的方法) | Thread类(线程的方法) |
| 唤醒方式 | notify()/notifyAll() 主动唤醒 | 超时自动唤醒(或interrupt()) |
| 使用前提 | 必须在synchronized同步块/方法内 | 无前提(可在任意代码块中调用) |
| 核心用途 | 线程间协作(等待条件满足) | 线程休眠(延迟执行) |
五、实战场景:从生产者-消费者到多资源协作
等待-通知机制在“生产者-消费者”“多资源竞争”等场景中应用广泛,以下是典型场景的优化实现。
1. 生产者-消费者问题(优化前后对比)
优化前(循环等待,CPU空转)
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 10; // 缓冲区容量
// 生产者:循环等待缓冲区空闲
public void produce(int item) {
while (true) {
synchronized (this) {
if (queue.size() < CAPACITY) {
queue.offer(item);
System.out.println("生产者生产:" + item + ",缓冲区大小:" + queue.size());
break;
}
}
// 空循环,CPU浪费
}
}
// 消费者:循环等待缓冲区有数据
public int consume() {
while (true) {
synchronized (this) {
if (!queue.isEmpty()) {
int item = queue.poll();
System.out.println("消费者消费:" + item + ",缓冲区大小:" + queue.size());
return item;
}
}
// 空循环,CPU浪费
}
}
}
优化后(等待-通知,高效协作)
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 10;
// 生产者:缓冲区满时wait()
public synchronized void produce(int item) throws InterruptedException {
// while循环检查缓冲区是否满
while (queue.size() >= CAPACITY) {
System.out.println("缓冲区满,生产者等待");
this.wait(); // 释放锁,进入等待
}
queue.offer(item);
System.out.println("生产者生产:" + item + ",缓冲区大小:" + queue.size());
this.notifyAll(); // 通知消费者:缓冲区有数据
}
// 消费者:缓冲区空时wait()
public synchronized int consume() throws InterruptedException {
// while循环检查缓冲区是否空
while (queue.isEmpty()) {
System.out.println("缓冲区空,消费者等待");
this.wait(); // 释放锁,进入等待
}
int item = queue.poll();
System.out.println("消费者消费:" + item + ",缓冲区大小:" + queue.size());
this.notifyAll(); // 通知生产者:缓冲区有空位
return item;
}
}
生产者-消费者交互图示
sequenceDiagram
participant 生产者线程
participant 缓冲区(锁对象)
participant 消费者线程
生产者线程->>缓冲区(锁对象): 1. 获取锁,检查容量(满)
生产者线程->>缓冲区(锁对象): 2. wait(),释放锁
消费者线程->>缓冲区(锁对象): 3. 获取锁,消费数据
消费者线程->>缓冲区(锁对象): 4. notifyAll(),释放锁
生产者线程->>缓冲区(锁对象): 5. 被唤醒,获取锁
生产者线程->>缓冲区(锁对象): 6. 检查容量(不满),生产数据
生产者线程->>缓冲区(锁对象): 7. notifyAll(),释放锁
2. 高级实现:Condition接口(灵活替代wait()/notify())
Java 5+ 提供的Condition接口(基于Lock)比synchronized更灵活,支持多条件等待(如生产者等“空位”、消费者等“数据”)。
关键代码(Condition实现生产者-消费者)
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class ConditionBuffer {
private Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 10;
private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
private final Condition notFull = lock.newCondition(); // 条件:缓冲区不满
private final Condition notEmpty = lock.newCondition(); // 条件:缓冲区不空
// 生产者
public void produce(int item) throws InterruptedException {
lock.lock(); // 获取锁(替代synchronized)
try {
// 缓冲区满时,等待notFull条件
while (queue.size() >= CAPACITY) {
System.out.println("缓冲区满,生产者等待");
notFull.await(); // 替代wait()
}
queue.offer(item);
System.out.println("生产者生产:" + item + ",缓冲区大小:" + queue.size());
notEmpty.signalAll(); // 通知消费者:缓冲区不空(替代notifyAll())
} finally {
lock.unlock(); // 释放锁(必须在finally中)
}
}
// 消费者
public int consume() throws InterruptedException {
lock.lock();
try {
// 缓冲区空时,等待notEmpty条件
while (queue.isEmpty()) {
System.out.println("缓冲区空,消费者等待");
notEmpty.await();
}
int item = queue.poll();
System.out.println("消费者消费:" + item + ",缓冲区大小:" + queue.size());
notFull.signalAll(); // 通知生产者:缓冲区不满
return item;
} finally {
lock.unlock();
}
}
}
六、性能实测:等待-通知vs循环等待
通过性能对比,直观展示等待-通知机制的优势(数据整合自两大文档):
| 评估维度 | 循环等待 | 等待-通知机制 | 类级别粗粒度锁 |
|---|---|---|---|
| CPU使用率 | 高(80%-100%,空转) | 低(5%-10%,仅执行时占用) | 低(10%-15%) |
| 响应延迟 | 高(依赖循环周期) | 低(条件满足即时唤醒) | 高(所有操作串行) |
| 可扩展性 | 差(线程越多CPU越满) | 好(支持大量线程等待) | 差(串行执行无并发) |
| 资源浪费 | 严重(无业务消耗CPU) | 低(等待时释放资源) | 中等(并发度低) |
| 适用场景 | 资源竞争极不频繁 | ✅ 推荐:高并发、竞争频繁 | 简单业务、低并发 |
barChart
title 不同方案CPU使用率对比(高并发场景)
x-axis 方案类型
y-axis CPU使用率(%)
"循环等待" : 95
"等待-通知" : 8
"类级别锁" : 12
七、最佳实践:5条黄金准则
- 永远在while循环中检查条件:避免虚假唤醒和条件变化导致的逻辑错误;
- 在synchronized/Lock内调用wait()/await():确保调用前已获取锁,否则抛出
IllegalMonitorStateException; - 优先使用notifyAll()/signalAll():除非明确只有一个等待线程,否则避免线程饥饿;
- finally中释放资源:锁释放、资源归还必须在finally中,避免异常导致资源泄漏;
- 高并发场景用Condition替代wait():多条件等待更灵活,减少不必要的唤醒(如生产者只唤醒消费者,不唤醒其他生产者)。
总结:从“低效等待”到“高效协作”
等待-通知机制通过“释放锁等待+主动唤醒”的逻辑,彻底解决了循环等待的CPU浪费问题,是Java并发编程中线程协作的核心技术。掌握其原理(四要素)、避坑要点(while vs if、notifyAll优先)和实战场景(生产者-消费者、资源分配),能显著提升高并发系统的性能与稳定性。
更多推荐

所有评论(0)