读完这篇,你能做什么
能为一个共享状态选择统一监视器,并使用条件循环正确等待状态变化。
学习理由、准备要求与知识点
为什么学它
synchronized 同时提供互斥与可见性,是理解 Java 线程协作和锁范围的基础。
先读这些内容
知识点清单
- 对象监视器
- 实例锁与类锁
- 可重入
- 临界区
- 同一把锁
- wait 释放监视器
notifyAll- 条件循环与虚假唤醒
运行前先确认JDK17 Object与JVMS17监视器语义。Java代码演示监视器的使用协议;Object.wait/notify的底层由虚拟机实现,不伪装成纯Java源码。
01先弄懂它是什么
synchronized 要先看锁的是哪个对象。每个对象对应一个监视器,同一时刻只有一个线程持有它;使用同一把锁的受保护代码才不会同时执行。实例同步方法锁 this,静态同步方法锁声明类的 Class 对象;两个不同实例通常使用不同的实例锁,所以不能只看方法都写了 synchronized。
还需要表达“没数据就等”的条件时,光有互斥不够。消费者如果拿着锁一直检查空队列,生产者就拿不到锁放数据。wait 会释放当前这个监视器,让别人有机会改变状态;被唤醒后,还要重新拿锁并检查条件。等待的是“现在是否有数据”这样的状态,不是等着领取某一次通知。
notify 让一个等待线程有机会重新争锁,notifyAll 则让所有等待线程都有机会回来检查条件;调用它们的线程仍持有锁,不会当场把锁交出去。通知也不会留给以后才来等待的线程。因此,生产者已经放好数据并通知后,晚到的消费者应先看“有没有数据”,有就继续,不能无条件 wait。检查、修改状态和通知,都要遵守同一套加锁规则,才能避免该继续时却一直睡着。
文中这几个词,先认一认
- 监视器 Monitor
- 与对象关联的锁和等待机制,同一线程可重入,但同一时刻不能由多个线程持有。
- 等待集 Wait Set
- 调用同一对象的 wait 后,暂时等待重新参与执行的线程集合。
- 条件谓词
- 决定现在能否继续的状态检查,例如队列是否非空、邮箱是否有空位。
- 虚假唤醒
- 即使没有通知、超时或中断,等待也可能醒来,这叫虚假唤醒;正常收到通知也不保证条件仍成立,所以 wait 要放在循环里重查状态。
02看看它怎么工作
正确模式是在持锁范围内用while检查条件,不满足就wait;返回后重新获得同一监视器,再次检查条件。循环既应对虚假唤醒,也应对真实通知后资源被另一个线程先拿走的情况。if只检查一次,无法保证唤醒后的条件仍然满足,可能从空队列读取或往满数组写入。
wait只释放调用对象的监视器,不会自动释放线程持有的其他锁。如果线程同时拿着账户锁和队列锁,在队列上wait仍保留账户锁,其他线程可能因此无法完成让条件成立的工作。sleep则连当前监视器也不释放,因此不能用在持锁轮询中模拟条件等待。
字节码中同步块使用monitorenter与monitorexit表达获取和释放,异常退出也需释放;同步方法由相应方法标志表达同步语义。HotSpot可根据版本、竞争与优化采用不同实现路径,但应用正确性应依赖监视器契约。不能把某版对象头位布局或某种锁优化当成所有JDK的固定语言规则。
正在加载流程图…
查看流程图源码(Mermaid)
stateDiagram-v2
[*] --> Compete: 进入同步范围
Compete --> Holding: 获得监视器
Holding --> Waiting: 条件不满足,wait释放该监视器
Waiting --> Compete: 通知、超时、中断或虚假唤醒
Holding --> Work: 条件满足
Work --> Holding: 修改状态并通知
Holding --> [*]: 正常或异常退出同步范围按顺序看,每一步做了什么
- 选择私有且稳定的锁对象,所有相关状态统一受其保护。
- 在持锁范围检查条件谓词,使用while等待。
- wait返回后重新检查条件与关闭、中断等状态。
- 修改共享状态后通知可能受影响的等待者。
- 退出临界区释放锁,让被唤醒线程有机会继续。
03对照实现代码
OpenJDK17.0.12+7 与 JVMS17 · Object#wait / notify / notifyAll;monitorenter / monitorexit
原创Java条件等待用法。固定Object源码中的关键等待和通知入口为native,实际挂起、竞争与唤醒在JVM中实现;此处不把用法代码称作HotSpot实现。
final class GateModel {
private final Object lock = new Object();
private boolean opened;
void awaitOpen() throws InterruptedException {
synchronized (lock) {
while (!opened) lock.wait(); // 原子地释放此监视器并进入等待,返回前重新持锁。
}
}
void open() {
synchronized (lock) {
opened = true; // 条件状态才是事实,先改状态再通知。
lock.notifyAll(); // 等待者仍需等本线程释放锁,再竞争并重查。
}
}
}阅读Object.java会发现wait(long)以及notify、notifyAll的native边界,Java源码主要提供入口、参数处理与契约。要继续追到底层,需要结合虚拟机源码与指令规范。不能为了看起来“源码级”而杜撰一个Java循环作为真正的线程挂起实现。
调用 wait 前,线程必须已经持有这个对象的监视器,否则抛 IllegalMonitorStateException。若重入多次,等待时会释放这把监视器对应的持有层数,恢复后再取得;它不会释放其他对象的锁。示例用私有 lock,减少外部代码意外参与同一等待过程。
notify 不保证按应用希望的顺序挑人。同一个等待集合里既有生产者又有消费者时,它可能叫醒一个仍然无法继续的线程。用 notifyAll 配合 while 重查,通常更容易写对,但会让更多线程一起争锁。如果需要分别通知“现在有数据了”和“现在有空位了”,可以考虑 ReentrantLock 的多个 Condition,把两种等待分开。
等待被中断时,InterruptedException路径与正常通知路径不同,应用应传播中断或在不能抛出时恢复中断标记并结束任务。捕获后空处理会隐藏取消请求。中断也不保证立即停止已在执行的业务,取消协议必须说明哪些阶段可以安全退出。
代码先调用谁,再调用谁
- synchronized获取指定对象监视器
- 持锁读取条件,不满足则调用
Object.wait - JVM将线程加入等待并释放该对象监视器
- 唤醒后重新竞争监视器并恢复执行
- while复查状态,执行操作后通知并退出同步范围
04动手写一个例子
可关闭、可排空的有界环形缓冲区
实现整数生产消费队列。满时put等待,空时take等待;close后拒绝新写入,但已存在元素仍按FIFO取完,随后take返回空。关闭必须唤醒等待者,等待可被中断。
- 输入 1
容量2;put1、put2、close、连续take三次- 输出 1
1、2、空
- 输入 2
容量1已满;另一线程put等待,此时close- 输出 2
等待生产者醒来并收到关闭异常
数组配合head、tail和size区分空与满。head指向下一个读取位置,tail指向下一个写入位置,移动时按容量取模。单纯依赖head等于tail无法区分空队列和满队列,因此显式保存size,所有字段都在同一监视器下更新。
put的等待条件是队列满且尚未关闭,醒来后若关闭则拒绝;take的等待条件是队列空且尚未关闭,关闭后仍有元素就继续取,关闭且为空才返回结束标记。把关闭状态纳入谓词,能让原本等待空间或数据的线程在关闭时有明确出口。
每次插入与删除都只处理一个槽位,因此不需要搬移数组。notifyAll放在状态变化之后,所有等待线程醒来都重新检查自己的谓词。该设计没有承诺公平获取,也没有持久性;它解决的是单进程内存中的有界协调,不是可靠消息投递。
为什么这样写是对的0≤size≤capacity,head与tail均为合法下标;从head起的size个元素按接受顺序等待取出;closed一旦为true不再接受写入。
只读写一个数组槽并移动下标;唤醒多个线程会增加调度和锁竞争成本,因此常量数据操作不等于恒定响应时延。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
import java.util.concurrent.*;
public class Main {
static final class Buffer {
final int[] values;
int head, tail, size;
boolean closed;
Buffer(int capacity) {
if (capacity<=0) throw new IllegalArgumentException();
values=new int[capacity];
}
synchronized void put(int value) throws InterruptedException {
while (size==values.length && !closed) wait(); // 唤醒后重新检查满与关闭条件。
if (closed) throw new IllegalStateException("closed");
values[tail]=value; tail=(tail+1)%values.length; size++;
notifyAll(); // 数据增加,消费者可能可以继续。
}
synchronized OptionalInt take() throws InterruptedException {
while (size==0 && !closed) wait();
if (size==0) return OptionalInt.empty(); // 关闭且排空,返回明确结束标记。
int result=values[head]; head=(head+1)%values.length; size--;
notifyAll(); // 空间增加,生产者可能可以继续。
return OptionalInt.of(result);
}
synchronized void close() { closed=true; notifyAll(); }
}
static void check(boolean ok) { if (!ok) throw new AssertionError(); }
public static void main(String[] args) throws Exception {
Buffer b=new Buffer(2); b.put(1); b.put(2); b.close();
check(b.take().orElseThrow()==1 && b.take().orElseThrow()==2 && b.take().isEmpty());
try { b.put(3); throw new AssertionError(); }
catch (IllegalStateException expected) { }
Buffer empty=new Buffer(1); empty.close(); empty.close(); check(empty.take().isEmpty());
Buffer round=new Buffer(1); round.put(9); check(round.take().orElseThrow()==9);
round.put(8); check(round.take().orElseThrow()==8); round.close();
Buffer concurrent=new Buffer(3);
ExecutorService executor=Executors.newFixedThreadPool(2);
try {
Future<?> producer=executor.submit(() -> {
try { for(int i=0;i<100;i++) concurrent.put(i); }
catch(InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); }
finally { concurrent.close(); }
});
Future<Integer> consumer=executor.submit(() -> {
int sum=0; OptionalInt item;
while ((item=concurrent.take()).isPresent()) sum+=item.getAsInt();
return sum;
});
producer.get(5,TimeUnit.SECONDS); check(consumer.get(5,TimeUnit.SECONDS)==4950);
} finally { concurrent.close(); executor.shutdownNow(); }
Buffer interrupted=new Buffer(1);
Thread.currentThread().interrupt();
try { interrupted.take(); throw new AssertionError(); }
catch (InterruptedException expected) { check(!Thread.currentThread().isInterrupted()); }
finally { Thread.interrupted(); interrupted.close(); }
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 容量为1时下标反复回绕仍保持FIFO。
- 关闭为空立即返回结束,重复关闭幂等。
- 关闭后排空已有数据但拒绝新写入。
- 进入等待时已有中断标记应抛
InterruptedException。
进阶选读 · 分离条件队列与取消竞态
场景大量生产者消费者共用一个等待集,notifyAll带来频繁无效唤醒,同时需要带超时的put和take。
做法用 ReentrantLock 配合 notEmpty、notFull 两个 Condition,分别等待数据和空位。带超时时,每次 awaitNanos 返回都重新检查状态,并扣掉已经等过的时间,不能又从完整预算开始等;时间预算用单调时钟计算。关闭队列时,唤醒两类等待者。还要写清:超时、关闭、中断和拿到资源几乎同时发生时,方法应返回哪种结果。
复杂度环形数据操作仍O(1),条件分离减少无关唤醒但不改变最坏等待不确定性;定时等待需要常量时间预算维护。
好处与代价更精细的条件队列改善争用,却增加状态分支和取消测试;生产代码通常优先使用成熟BlockingQueue并额外设计关闭协议。
05项目里怎么用
锁对象尽量私有、稳定且不复用公共字符串或外部可访问对象。外部代码若也锁同一对象,会把原本局部的同步范围扩大,制造难以解释的延迟或死锁。临界区内避免远程调用、大文件IO和等待另一个线程完成未知工作,使状态更新保持短小可推理。
条件应保存在数据状态里,notify 只提醒等待者重新查看。若把某一次通知本身当业务事件,通知早于 wait 时就会错过。要累计名额可用 Semaphore,要存多条消息用队列,要等大家都结束用 Latch,别让一套 wait/notify 承担所有不同需求。
排错时区分BLOCKED竞争监视器、WAITING等待条件和TIMED_WAITING定时等待;线程状态本身不是业务故障证明。结合锁持有者、栈帧、等待对象和采样趋势检查条件为何无法改变。多个线程都在wait可能是正常空闲,也可能是关闭遗漏或错误通知导致的停滞。
容易踩的坑
- if检查后wait,醒来不复查条件。
- notify后立即假设等待者已经执行。
- 持有其他锁时wait,误以为释放了全部锁。
- 未持有对应监视器调用wait或notify。
- 关闭只改标志而不唤醒等待线程。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| synchronized与wait | 语言支持直接,适合小状态协议 | 一个等待集,通知选择较粗 | 教学与简单共享状态 |
ReentrantLock与Condition | 可分离条件、可中断获取与定时等待 | 必须正确finally解锁,分支更多 | 复杂条件协调 |
成熟BlockingQueue | 封装有界容量与阻塞操作 | 关闭及业务可靠性仍需额外协议 | 生产者消费者任务传递 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么真实通知后仍需要while重查?
展开参考答案与推导
被唤醒线程重新拿锁前,其他线程可能先消费资源;此外还存在虚假唤醒。
02wait与sleep在锁上的区别是什么?
展开参考答案与推导
wait释放调用对象的监视器并在返回前重新获取,sleep不释放已持有监视器;wait也不释放其他对象的锁。
03为什么通知不能替代共享状态?
展开参考答案与推导
通知不为未来等待者保存,状态才记录条件是否已经成立,后来到达的线程应先检查状态。
换个情况,还会处理吗
01队列关闭后进程仍不退出,如何排查?
展开参考答案与推导
检查等待谓词是否包含closed、close是否notifyAll,以及异常路径是否关闭;再看线程是否持有其他阻止条件变化的锁。
02生产者消费者用notify偶尔全部卡住,可能原因是什么?
展开参考答案与推导
一个等待集混合不同条件,唤醒的线程可能仍无法继续;用while保证安全,并采用notifyAll或分离Condition保证合适等待者有机会执行。
03两个实例的同步方法同时修改静态集合,为什么仍有竞态?
展开参考答案与推导
实例方法锁各自this,不能共同保护静态共享状态,应使用统一锁或合适并发结构。
写代码试一试
01实现可关闭的有界环形队列。
- 输入 / 条件
容量2,写1和2后关闭- 输出 / 预期
依次读1、2、空
提示 size区分空满,closed进入等待谓词。
展开参考答案与推导
按正文监视器协议,每次修改后notifyAll。
02为关闭唤醒生产者写确定性测试。
- 输入 / 条件
容量1先写入,另一线程尝试写,主线程关闭- 输出 / 预期
等待写入收到关闭异常
提示 Latch标记线程开始尝试,Future设置等待期限。
展开参考答案与推导
无论关闭发生在wait前后,关闭标志都应阻止写入;finally关闭并停止测试执行器。
03实现带超时的等待门。
- 输入 / 条件
门未开,预算0- 输出 / 预期
立即返回false
提示 使用单调时钟与剩余预算循环。
展开参考答案与推导
每次唤醒重查状态与剩余时间,不能每轮重新等待完整预算;门已开时按约定返回true。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能为一个共享状态选择统一监视器,并使用条件循环正确等待状态变化。
本篇重点加练
wait 只释放特定监视器,重新获得锁后仍须依条件判断,嵌套等待容易隐藏依赖。
- wait 释放监视器
- 条件循环与虚假唤醒
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
一个类含实例同步方法、静态同步方法以及嵌套同步调用;创建两个实例并调用这些方法,标出每个临界区使用的监视器。
- 提交内容
- 锁身份图与执行约束
- 怎样算完成
- 区分对象与 Class 对象
- 记录重入路径
练到的知识点对象监视器 · 实例锁与类锁 · 可重入
- 作业 02设计评审
库存写方法锁住 this,报表方法锁住新建
Object,二者访问同一库存集合。审查读写路径如何共享保护协议。- 提交内容
- 访问清单与锁协议
- 怎样算完成
- 每个共享字段有保护规则
- 包含只读遍历与异常返回
练到的知识点临界区 · 同一把锁
- 作业 03原理问答
三个工作者等待同一任务状态,调度者更新状态后发通知。画出等待集、锁竞争和继续执行三个阶段,解释通知记录缺失的影响。
- 提交内容
- 监视器状态图
- 怎样算完成
- 区分被唤醒与已获得锁
- 状态检查使用明确共享条件
练到的知识点wait 释放监视器 · notifyAll
- 作业 04代码推演
线程持有两把监视器锁,在其中一把锁上调用 wait。画出等待前后的锁持有状态,并分析另一个线程更新等待条件需要哪些资源。
- 提交内容
- 锁持有及条件更新图
- 怎样算完成
- 标明 wait 对象
- 不把等待理解为释放全部锁
练到的知识点wait 释放监视器
- 作业 05原理问答
等待条件包含“数据可用”与“服务已关闭”,生产者只在新增数据时通知。审查每个条件变更与等待者重新检查之间的协议。
- 提交内容
- 条件变化与通知矩阵
- 怎样算完成
- 覆盖关闭及失败状态
- 条件由共享状态表达
练到的知识点条件循环与虚假唤醒
综合应用
- 作业 06代码推演
线程在观察条件满足后尚未重新获得锁,另一线程已改变条件。构造完整交错,审查等待代码中条件检查的位置。
- 提交内容
- 交错轨迹与代码审查
- 怎样算完成
- 覆盖真实竞争及无条件通知
- 说明中断路径
练到的知识点条件循环与虚假唤醒
- 作业 07编程实践
实现本地批次启动门:允许添加参与者、统一开始并在超时后关闭;为提前通知、重复开始、等待中中断和关闭准备确定性测试。
- 提交内容
- 源码与线程状态记录
- 怎样算完成
- 所有等待线程最终可结束
- 不以 sleep 作为唯一同步手段
练到的知识点对象监视器 · 可重入 · 临界区 · 同一把锁 · wait 释放监视器 · notifyAll · 条件循环与虚假唤醒
- 作业 08设计评审
业务回调在监视器保护内执行,回调又会进入等待接口。审查可重入、等待释放和回调对共享状态的影响。
- 提交内容
- 重入调用图与状态约束
- 怎样算完成
- 包含回调异常
- 区分进入锁的次数和条件成立
练到的知识点wait 释放监视器 · 条件循环与虚假唤醒
- 作业 09编程实践
实现本地两阶段资源等待演示,分别等待资源就绪和许可生效,用确定性触发器测试取消、额外通知及嵌套锁竞争。
- 提交内容
- Java 程序与等待轨迹
- 怎样算完成
- 总运行时间有界
- 报告每次醒来重新观察到的条件
练到的知识点wait 释放监视器 · 条件循环与虚假唤醒
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
某同步方法持锁读取磁盘文件,其他线程等待状态变化。设计能辨别锁占用与 IO 耗时的本地实验,并提出评审指标。
- 提交内容
- 实验方案与观测报告
- 怎样算完成
- 记录等待者与持锁者证据
- 区分吞吐改善和正确性约束
练到的知识点临界区 · 同一把锁 · wait 释放监视器 · notifyAll · 条件循环与虚假唤醒
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
构造从未发生虚假唤醒、通知也全部送达,但使用单次 if 检查仍违背业务条件的交错。
- 提交内容
- 反例时间线
- 怎样算完成
- 不假设通知传递丢失
- 明确其他线程改变条件的时刻
练到的知识点wait 释放监视器 · 条件循环与虚假唤醒
- 作业 12思维拓展 · 条件变化
接口新增“等待期间可以撤销资格”,且撤销必须在任务开始前生效。重新评审条件集合与获得资源后的检查边界。
- 提交内容
- 状态机与验收场景
- 怎样算完成
- 区分撤销请求和撤销生效
- 包括通知后尚未获得监视器
练到的知识点wait 释放监视器 · 条件循环与虚假唤醒
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读规范英文
Java 17 JLS:Threads and Locks (新标签页)↗
阅读 happens-before、volatile 与监视器规则,区分可见性、原子性和顺序。
- 实践参考文档英文
Java 17:Object (新标签页)↗
阅读 wait、notify 与监视器所有权,用条件循环处理线程等待。
- 深入参考文档英文
Java 17:Thread (新标签页)↗
阅读中断、join 和线程生命周期,实现协作取消并正确保留中断状态。
- 深入参考文档英文
OpenJDK17 Object源码 (新标签页)↗
识别Java入口与native虚拟机边界。
- 深入参考文档英文
JVMS17监视器指令 (新标签页)↗
理解monitorenter与monitorexit的规范语义。