synchronized面试题
synchronized 锁对象与锁 Class 的区别
核心本质区别(一句话说清)🔑
锁对象:锁的是当前实例对象,每个实例拥有独立的锁,互不干扰
锁 Class:锁的是类的 Class 对象,全局唯一,所有实例共享同一把锁
💡 划重点:本质上都是锁对象!Class 本身也是一个特殊的java.lang.Class实例,只是它全局唯一,所以锁 Class 等价于给整个类加了一把全局锁。
🔐 synchronized 的两张“面孔”
我习惯把这个问题拆成两个维度理解:
- 锁的是什么
- 锁的范围有多大
| 分类 | 锁的对象 | 实际锁住的是... | 并发隔离范围 |
|---|---|---|---|
synchronized(this) 或 普通方法 | 对象实例 | new Object() 出来的那个实例 | 同一个实例的多线程互斥 |
synchronized(Xxx.class) 或 静态方法 | 类对象 | JVM 里唯一的 Xxx.class 对象 | 所有该类的静态同步方法 + 所有 synchronized(Xxx.class) 代码块互斥 |
🧠 底层原理一句话
JVM 里每个对象都有一个 monitor(监视器),synchronized 就是去抢这个 monitor 的所有权。
- 锁实例:抢的是
实例对象的monitor - 锁 Class:抢的是
Class 对象的monitor
两者是不同的锁对象,所以它们之间不会互相阻塞。
全方位对比表 📊
| 对比维度 | 锁对象(synchronized(this)) | 锁 Class(synchronized(XXX.class)) |
|---|---|---|
| 锁的目标 | 类的某个具体实例对象 | 类的java.lang.Class对象 |
| 作用范围 | 仅当前实例的同步代码块 / 方法 | 该类所有实例的同步代码块 / 方法 |
| 并发特性 | 不同实例可同时执行同步代码 | 所有实例同一时间只能有一个执行 |
| 底层实现 | 锁实例对象头的 Mark Word | 锁 Class 对象头的 Mark Word |
| 适用场景 | 保护实例级别的共享变量 | 保护静态变量 / 全局资源 |
底层原理深度解析 🧠
synchronized 的底层实现依赖于对象头和监视器锁(Monitor):
- 每个 Java 对象都有一个对象头,其中
Mark Word存储了对象的哈希码、分代年龄和锁信息 - 当执行
monitorenter指令时,JVM 会尝试获取目标对象的 Monitor 所有权 - 锁对象时,目标是
this实例的对象头;锁 Class 时,目标是XXX.class这个全局唯一实例的对象头
⚠️ 关键认知:不存在所谓的 "类锁",只有 "对象锁"。锁 Class 只是锁了一个特殊的全局对象而已。
代码示例直观对比 💻
锁对象(不同实例互不干扰)
public class SyncDemo {
public void syncMethod() {
synchronized (this) { // 锁当前实例
System.out.println(Thread.currentThread().getName() + " 执行中");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
SyncDemo demo1 = new SyncDemo();
SyncDemo demo2 = new SyncDemo();
// 两个不同实例,可同时执行
new Thread(demo1::syncMethod, "线程1").start();
new Thread(demo2::syncMethod, "线程2").start();
}
}
// 输出:线程1和线程2几乎同时打印锁 Class(所有实例互斥)
public class SyncDemo {
public void syncMethod() {
synchronized (SyncDemo.class) { // 锁Class对象
System.out.println(Thread.currentThread().getName() + " 执行中");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
public static void main(String[] args) {
SyncDemo demo1 = new SyncDemo();
SyncDemo demo2 = new SyncDemo();
// 两个不同实例,必须排队执行
new Thread(demo1::syncMethod, "线程1").start();
new Thread(demo2::syncMethod, "线程2").start();
}
}
// 输出:线程1执行完1秒后,线程2才执行🔀 互斥关系流程图

- 实线互斥虚线不互斥
- 相同锁对象才互斥,不同锁对象各玩各的 🎉
🧨 经典踩坑:用错锁导致“失灵”
public class BadService {
public static void doStatic() {
synchronized (this) { // ❌ 编译报错!静态方法里没 this
}
}
public void doInstance() {
synchronized (BadService.class) { // ✅ 可以,但这实际是锁 Class
}
}
}很多新手以为在普通方法里写 synchronized(Xxx.class) 就能锁住实例,结果把实例级别的互斥提升到了全局互斥,性能陡降 📉
面试必踩坑点 ⚠️
- ❌ 锁可变对象:如果锁的对象引用发生了变化(比如
String s = "a"; s = "b";),会导致锁失效 - ❌ 静态与非静态方法不互斥:非静态方法锁 this,静态方法锁 Class,两者是不同的锁,不会互斥
- ❌ 同一个类的多个静态方法:如果都用 synchronized 修饰,它们会互斥,因为都锁了同一个 Class 对象
- ❌ 继承场景:子类调用父类的同步方法,锁的是子类实例,不是父类实例
面试标准回答话术 ✅
面试官您好,synchronized 锁对象和锁 Class 的核心区别在于锁的目标对象不同:
- 锁对象锁的是当前实例,每个实例有自己的锁,不同实例之间的同步代码可以并发执行,适合保护实例级别的共享变量
- 锁 Class 锁的是类的 Class 对象,全局唯一,所有实例共享同一把锁,同一时间只能有一个实例执行同步代码,适合保护静态变量或全局资源
- 本质上两者都是对象锁,底层都是通过对象头的 Mark Word 和 Monitor 实现的,不存在特殊的 "类锁" 概念
- 常见的坑点是静态方法和非静态方法的锁不互斥,以及锁可变对象导致的锁失效问题
如果你还能顺嘴提到这些,面试官会频频点头:
- 锁升级:
synchronized会从偏向锁→轻量级锁→重量级锁逐步膨胀,但锁对象不变。锁 Class 和锁实例在膨胀过程中没有任何差异,只是起点 monitor 不同。 - 内存可见性:无论是锁实例还是 Class,解锁时都会把本地内存变量刷回主存,语义完全一样。
- wait/notify:必须持有对应锁对象才能调用。如果你锁的是 Class,就只能调用
Counter.class.wait(),调用this.wait()会报IllegalMonitorStateException。
synchronized 锁升级详细过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
🚀 锁升级的核心背景
JDK1.6 之前,synchronized 是重量级锁,需要操作系统内核态 / 用户态切换,性能极差。JDK1.6 及之后,JVM 引入了锁升级机制,根据线程竞争的激烈程度,自动从低开销锁升级到高开销锁,实现了 "按需分配",大幅提升了同步性能。
锁升级的本质:修改对象头中 Mark Word 的标志位和存储内容,整个过程是单向不可逆的(除偏向锁可重置为无锁)。
⚡ 前置知识:32 位 JVM 对象头 Mark Word 结构
每个 Java 对象在堆里都有一个对象头,核心是一块叫 Mark Word 的数据,锁的状态就靠它来记。
┌──────────────────────────┬─────────────────┐
│ Mark Word (64bit) │ Klass Pointer │
│ 锁标志位、hashCode、GC │ 指向类元数据 │
└──────────────────────────┴─────────────────┘锁升级的所有操作都围绕 Mark Word 展开,它是对象头中最核心的部分,结构如下:
| 锁状态 | 25 位内容 | 4 位分代年龄 | 1 位偏向锁标志 | 2 位锁标志位 |
|---|---|---|---|---|
| 无锁 | 对象哈希码 | ✅ | 0 | 01 |
| 偏向锁 | 线程 ID+Epoch | ✅ | 1 | 01 |
| 轻量级锁 | 指向栈帧锁记录的指针 | - | - | 00 |
| 重量级锁 | 指向操作系统互斥量的指针 | - | - | 10 |
| GC 标记 | 空 | - | - | 11 |
🔄 锁升级完整流程(分阶段详解)
无锁(01) ──┬─ 【偏向锁开启】 ──► 偏向锁(01) ──┬─ 竞争出现 ──► 轻量级锁(00) ──┬─ 自旋失败/竞争加剧 ──► 重量级锁(10)
│ │ │
└─ 直接 CAS 抢锁 └─ 撤销偏向 └─ 膨胀为 Monitor无锁状态 🆓
- 触发条件:对象刚创建,还没有被任何线程访问过同步块
- 核心特征:Mark Word 存储对象的哈希码、分代年龄,偏向锁标志 = 0,锁标志 = 01
- 执行逻辑:没有任何同步开销,线程可以自由访问对象
偏向锁 🎯
- 触发条件:只有一个线程反复访问同步块,没有其他线程竞争
- 核心原理:JVM 通过 CAS 将对象头中的线程 ID设置为当前线程 ID,以后该线程再次进入同步块时,不需要任何 CAS 操作,直接判断线程 ID 是否匹配即可
- 优点:几乎零额外开销,性能接近无锁
- 缺点:一旦有其他线程竞争,会触发偏向锁撤销,这个过程需要STW(Stop The World)(虽然时间极短,但也是开销)
- 注意:JDK15 及之后,偏向锁默认关闭,因为高并发场景下撤销开销大于收益
[无锁 01] ── CAS 写入 ThreadID ──► [偏向锁 01 | ThreadID | Epoch]
失败(其他线程占用)
└── 撤销偏向 ──► 轻量级锁轻量级锁 ⚡
- 触发条件:有第二个线程来竞争偏向锁,偏向锁撤销失败
- 核心原理:
- JVM 在当前线程的栈帧中创建一个锁记录(Lock Record)
- 通过 CAS 将对象头的 Mark Word复制到锁记录中
- 再次通过 CAS 将对象头的 Mark Word 设置为指向锁记录的指针
- 如果 CAS 成功,当前线程获得轻量级锁;如果失败,说明有多个线程竞争,进入自适应自旋(JDK1.6 之后默认)
- 优点:自旋不需要内核态切换,响应速度快
- 缺点:如果竞争激烈,自旋会浪费大量 CPU 资源(空转)
[偏向锁 01] ── CAS 替换为 Lock Record 指针 ──► [轻量级锁 00 | ptr-to-LockRecord]
│
│ CAS 失败,自旋等待
│ 自旋超时或竞争加剧
▼
重量级锁重量级锁 🔒
- 触发条件:
- 自旋次数达到阈值(默认 10 次,自适应自旋会动态调整)
- 有更多线程加入竞争,自旋失败
- 核心原理:JVM 向操作系统申请互斥量(mutex),将对象头的 Mark Word 设置为指向互斥量的指针,所有竞争线程都会被阻塞,进入操作系统的等待队列
- 优点:不会浪费 CPU 资源,适合长时间持锁的场景
- 缺点:需要内核态 / 用户态切换,响应速度慢,开销极大
[轻量级锁 00] ── 膨胀 ──► [重量级锁 10 | ptr-to-ObjectMonitor]
│
├─ Owner: 当前持有线程
├─ EntryList: 阻塞争抢队列
└─ WaitSet: wait() 调用的等待队列为什么说重量? 因为阻塞/唤醒都要系统调用,用户态↔内核态切换,耗时相当于 几千 ~ 上万条指令,巨重无比 😱。
📊 锁升级流程图

对象创建
│
无锁状态 (01)
│ 第一个线程访问同步块
偏向锁 (01,偏向=1)
╱ ╲
同一线程再访问 第二个线程竞争
(保持偏向锁) │
偏向锁撤销 (STW)
│
原线程仍在持锁
│
轻量级锁 (00)
╱ ╲
CAS成功 CAS失败
│ │
获得轻量级锁 自适应自旋
╱ ╲
自旋成功 自旋失败/更多竞争
│ │
获得轻量级锁 重量级锁 (10)
│
所有线程阻塞等待
│
操作系统调度唤醒
│
锁释放
│
下一个线程获得锁📋 四种锁状态核心对比
| 锁状态 | 锁标志位 | 核心开销 | 适用场景 | 性能表现 |
|---|---|---|---|---|
| 无锁 | 01 | 0 | 没有任何线程竞争 | 最优 🚀 |
| 偏向锁 | 01 (偏向 = 1) | 仅第一次 CAS | 单线程反复访问同步块 | 接近无锁 🎯 |
| 轻量级锁 | 00 | 多次 CAS + 自旋 | 多线程轻度竞争,持锁时间短 | 较好 ⚡ |
| 重量级锁 | 10 | 内核态切换 + 线程阻塞 | 多线程重度竞争,持锁时间长 | 较差 🔒 |
⚠️ 面试必问的关键补充点
- 锁升级单向性:只能从无锁→偏向锁→轻量级锁→重量级锁,不能降级(除了偏向锁可以在全局安全点重置为无锁)
- 自适应自旋:JVM 会根据前一次自旋的结果动态调整自旋次数,前一次成功则多自旋,失败则少自旋甚至不自旋
- 锁消除:JIT 编译器在运行时,如果发现某个对象不会被其他线程访问,会直接消除该对象的同步锁
- 锁粗化:如果 JIT 发现连续多个同步块都是对同一个对象加锁,会合并成一个大的同步块,减少加锁解锁次数
- 偏向锁关闭:JDK15 + 默认关闭偏向锁,可以通过
-XX:+UseBiasedLocking手动开启,但不推荐在高并发场景下使用
synchronized 与 Lock 接口的底层 AQS 实现对比
这俩是 Java 并发编程里最核心的互斥锁实现,我从「底层原理、核心差异、适用场景」三个维度说,尽量不啰嗦~ 😊
一句话说清本质
🔑 synchronized 是 JVM 内建的关键字级锁,基于对象监视器(Monitor) → 依赖底层 OS Mutex,线程阻塞/唤醒要切换内核态。
🔑 Lock(以 ReentrantLock 为例) 是 JDK 级的类,基于 AQS(AbstractQueuedSynchronizer) → 用 CAS + 变种 CLH 队列 + LockSupport.park/unpark 实现,多数竞争通过自旋+CAS 在用户态解决。
💡 “两者最终阻塞都会用到系统调用,但 AQS 把‘进内核态’的时机压到极低,且控制更精细。”
底层架构总览

synchronized 底层全景
对象结构 & 锁状态
每个 Java 对象头里有一个 Mark Word,锁标志位决定了锁升级路径:

- 偏向锁:记录线程 ID,重入时只需比对,无需 CAS。
- 轻量级锁:在线程栈帧里建 Lock Record,CAS 把 Mark Word 指向它,失败就自旋。
- 重量级锁:膨胀为
ObjectMonitor,它内部有三个队列:- _
cxq(竞争队列,单向链表) - _
EntryList(就绪队列,双向链表) - _
WaitSet(wait() 的线程,双向链表)
- _
- 线程进入/唤醒依赖于 OS 函数
pthread_mutex_lock/pthread_cond_wait,会有用户态↔内核态切换开销。
⚠️ “网上常说‘synchronized 很重’,其实 JVM 有大量优化:锁粗化、锁消除、自适应自旋,纯粹重量级才重。”
Lock 接口的 AQS 实现原理
AQS 是一个用于构建锁/同步器的框架,ReentrantLock 内部有一个 AQS 子类。
核心三件套
volatile int state
+ 变种 CLH 队列(FIFO 双端队列)
+ CAS 操作(Unsafe.compareAndSwapInt)AQS 队列示意图 🔽
- state 用 CAS 修改,代表锁状态/重入次数。
- 队列 是双向链表,每个节点有
waitStatus(SIGNAL/CANCELLED 等),线程封装在节点里。 - 阻塞/唤醒使用
LockSupport.park/unpark,底层Unsafe.park调用 POSIX 规范里的futex,是一种用户-内核混合的轻量阻塞,能在无竞争时完全不陷入内核。
公平 vs 非公平
- 非公平锁:一上来
CAS(0,1)抢一次,不排队的“插队”逻辑,吞吐量高。 - 公平锁:严格检查队列里有没有等待更久的线程
hasQueuedPredecessors(),有就乖乖排队。
条件变量
AQS 的 ConditionObject 内部维护单独的条件队列,一个 Lock 可以有多个 Condition(synchronized 只有一个 wait-set)。
await() 释放锁、入队、park;signal() 把节点从条件队转移到同步队。
关键技术点对比表
| 维度 | 🟢 synchronized(JVM 内置) | 🔵 Lock(AQS 实现) |
|---|---|---|
| 实现语言 | C++ HotSpot | 纯 Java |
| 锁状态存储 | 对象头 Mark Word(32/64 位压缩指针) | AQS 的volatile int state变量 |
| 等待队列 | Monitor 的_cxq/_EntryList(双向链表,非公平) | AQS 的CLH 变种队列(双向链表,公平 / 非公平可选) |
| 锁升级机制 | ✅ 有(偏向→轻量级→重量级,自适应自旋) | ❌ 无(直接用 CLH 队列 + CAS+LockSupport) |
| 可中断性 | ❌ 不可(除非抛出异常) | ✅ 可(lockInterruptibly()) |
| 超时等待 | ❌ 不可 | ✅ 可(tryLock(long time, TimeUnit unit)) |
| 公平性 | ❌ 仅重量级锁有 “伪公平”(EntryList 按顺序唤醒,但新线程可能插队) | ✅ 可选(构造函数传true/false) |
| 绑定条件 | ❌ 仅 1 个(wait()/notify()/notifyAll()) | ✅ 多个(newCondition()) |
| 释放锁 | ✅ 自动(JVM 退出同步块 / 方法时) | ❌ 手动(必须在finally块调用unlock()) |
一句话总结适用场景(言简意赅)
- synchronized:简单互斥场景(JVM 自动优化,性能不差,代码安全)
- Lock(AQS):复杂并发场景(需要可中断、超时、公平锁、多条件绑定)
面试官视角的选型心法
🎯 “能用 synchronized 解决的别折腾 Lock —— 简单、安全,JVM 帮你兜底优化。
可一碰到死磕公平、需要定时放弃、多条件协调,这就是 AQS 的天下了,比如 ArrayBlockingQueue 两个 Condition 分工‘非空’‘非满’,synchronized 做不到这么细。”
😎 “最后记住 —— 两者到最后都会陷入内核,区别在于 AQS 让‘必须内核干预’的临界点更靠后,日常的 CAS+自旋把大部分竞争消化在了用户态,这就是性能差异的本质。”
wait / notify / notifyAll 使用规范
这套机制我主要用在需要线程间协作的场景,比如手写过生产者-消费者队列。
🔍 核心原理(10 秒快速回顾)
这三个方法是Object 类的本地方法,必须在 synchronized 块里调用,不是 Thread 类的!而且调用对象必须和锁对象是同一个。它们基于对象监视器(Monitor) 实现,用于线程间的等待 / 通知机制:
wait():释放当前持有的锁,线程进入等待队列(Wait Set) 阻塞notify():随机唤醒等待队列中的一个线程notifyAll():唤醒等待队列中的所有线程
// ❌ 错误示范:没持有锁就调用
Object lock = new Object();
lock.wait(); // 直接抛 IllegalMonitorStateException
// ✅ 正确姿势
synchronized (lock) {
lock.wait(); // 当前线程释放lock锁,进入等待
}你把它想成一个规则:你要让人家等待,你得先拿到人家的管理权(锁)。 🔑
📌 wait() 的核心秘密:释放锁 + 等待唤醒
很多人分不清 wait() 和 sleep(),我画了张表对比:
| 对比维度 | wait() | sleep() |
|---|---|---|
| 所属类 | Object | Thread |
| 释放锁 | ✅ 立刻释放当前对象锁 | ❌ 抱着锁睡觉 |
| 唤醒条件 | notify/notifyAll 或中断 | 时间到或中断 |
| 使用环境 | 必须在同步块内 | 任意地方 |
流程图 🎯
一句话:wait() 是“我先歇会儿,锁给你们用”,而 sleep() 是“我睡会儿,锁还得在我这儿”。😴
📊 线程状态转换图
notify vs notifyAll:天线宝宝的独白与大喇叭
notify():随机唤醒一个正在等待该锁的线程(具体哪个由JVM决定,不保证公平)。notifyAll():把等待队列里所有线程全叫醒,它们再去抢锁。
🌰 经典踩坑代码:单条件用 notify 的虚假唤醒风险
synchronized (lock) {
if (queue.isEmpty()) { // ❌ 用 if 判断,只检查一次
lock.wait();
}
// 消费数据...
}如果线程被虚假唤醒(spurious wakeup,没收到 notify 也自己醒了),或者被 notifyAll 意外唤醒了但条件仍不满足,if 就直接往下走了,程序逻辑就错了。
⭕️ 铁律:永远在 while 循环里调用 wait()!
synchronized (lock) {
while (queue.isEmpty()) { // ✅ 被唤醒后再次检查条件
lock.wait();
}
// 安全地消费数据
}📌 notify 还是 notifyAll?一个场景判断标准
我用个生活中的例子 🌰:
场景:一个盘子放菜,厨师做菜,服务员取菜。
- 只有一个厨师和一个服务员等着,条件相同(盘子空/满),用 notify 就行。
场景:仓库有“空仓”和“满仓”两种条件。
- 有生产者等着往空仓放货,有消费者等着满仓取货。
- 如果用
notify,可能你唤醒的是“另一个生产者”,而“消费者”永远醒不过来 —— 信号丢失,死锁前兆。💀
规范总结:
- 所有等待线程的等待条件逻辑上一致 → 可以用
notify优化性能。 - 等待条件不同 → 老老实实用
notifyAll,然后让while循环去过滤条件,安全性最高。 - 不确定就用
notifyAll,毕竟正确性优先于性能。⚠️
⚠️ 6 条强制使用规范(违反必出 bug)
| 序号 | 规范内容 | 违反后果 | 原理说明 |
|---|---|---|---|
| 1 ✅ | 必须在synchronized同步块 / 方法中调用 | 抛出IllegalMonitorStateException | 只有持有对象锁的线程,才能操作该对象的监视器 |
| 2 ✅ | 必须在while循环中检查等待条件,绝对不能用 if | 虚假唤醒导致业务逻辑错误 | 线程被唤醒后,可能条件仍不满足,需要重新检查 |
| 3 ✅ | 优先使用notifyAll(),除非能 100% 确定只有一个等待线程 | 线程饿死,程序永久阻塞 | notify()随机唤醒一个,可能唤醒的是不满足条件的线程 |
| 4 ✅ | 锁对象和调用 wait/notify 的对象必须是同一个 | 抛出IllegalMonitorStateException | 每个对象有独立的监视器,不能跨对象操作 |
| 5 ✅ | 必须捕获InterruptedException并正确处理,不能吞异常 | 线程无法被中断,资源泄漏 | 中断是线程间协作的唯一安全方式 |
| 6 ✅ | 永远不要用Thread.sleep()代替wait() | 死锁风险,CPU 空转 | sleep()不释放锁,会一直持有直到超时 |
❌ 面试高频踩坑点
虚假唤醒问题(最经典):线程可能在没有被 notify/notifyAll 的情况下醒来,这是 JVM 允许的正常现象
// ❌ 错误写法
if (条件不满足) {
wait();
}
// ✅ 正确写法
while (条件不满足) {
wait();
}notify 导致线程饿死:如果多个线程等待不同条件,notify 可能永远唤醒不到需要的线程
// 生产者消费者模型中,用notify可能导致生产者唤醒生产者,消费者唤醒消费者
// ✅ 必须用notifyAll()锁对象不一致:
// ❌ 错误:锁是this,调用wait的是lock对象
synchronized (this) {
lock.wait();
}
// ✅ 正确:锁和调用对象一致
synchronized (lock) {
lock.wait();
}吞掉 InterruptedException:
// ❌ 错误:吞掉异常,线程无法被中断
try {
wait();
} catch (InterruptedException e) {
// 什么都不做
}
// ✅ 正确:恢复中断状态或抛出异常
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态
throw new RuntimeException("线程被中断", e);
}💡 最佳实践
- 优先使用 JUC 包的Condition接口:支持多个条件队列,比 wait/notify 更灵活
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // 消费者等待队列
Condition notFull = lock.newCondition(); // 生产者等待队列
// 生产者
lock.lock();
try {
while (队列满了) {
notFull.await();
}
生产数据;
notEmpty.signal(); // 只唤醒消费者,不会唤醒其他生产者
} finally {
lock.unlock();
}- 使用带超时时间的wait(long timeout):避免程序永久阻塞
- 不要在循环外调用 wait ():所有 wait () 都必须在 while 循环中
- 尽量避免直接使用 wait/notify:优先使用 JUC 提供的并发工具类(BlockingQueue、CountDownLatch、CyclicBarrier 等)
📌 最终版的模范代码(生产者-消费者骨架)
class BoundedBuffer {
final Object lock = new Object();
Queue<Integer> queue = new LinkedList<>();
int capacity = 10;
void produce(int value) throws InterruptedException {
synchronized (lock) {
while (queue.size() == capacity) { // 条件不满足就等
lock.wait();
}
queue.add(value);
lock.notifyAll(); // 唤醒所有等待者(包含消费者)
}
}
int consume() throws InterruptedException {
synchronized (lock) {
while (queue.isEmpty()) {
lock.wait();
}
int value = queue.poll();
lock.notifyAll();
return value;
}
}
}核心要点卡片 🃏:
🔐 锁内调用:wait/notify/notifyAll 必须在 synchronized 中使用同一锁对象。
🔄 循环检查:wait 必须在 while 循环中,防治虚假唤醒。
📣 唤醒策略:单一条件用 notify,多条件用 notifyAll,保底用 notifyAll。
🧠 释放锁:wait 会立即释放锁,sleep 不会,死记区分。
🔥 面试加分项
- 能说出虚假唤醒的本质:JVM 为了优化,允许线程在没有被显式唤醒的情况下从 wait () 返回
- 能清晰对比 wait () 和 sleep () 的区别:
| 特性 | wait() | sleep() |
|---|---|---|
| 所属类 | Object | Thread |
| 释放锁 | 是 | 否 |
| 调用条件 | 必须在 synchronized 中 | 任意位置 |
| 唤醒方式 | notify/notifyAll/ 超时 / 中断 | 超时 / 中断 |
- 能说出 notify 和 notifyAll 的性能差异:notifyAll 会导致大量线程竞争锁,性能稍差,但更安全
