02-02-B-AQS与JUC锁面试与生产事故实战
️ 关键词:AQS 面试题 · ReentrantLock · 公平锁 · 读写锁 · StampedLock · Semaphore 限流 · CountDownLatch · 死锁复盘
📌 导读 :这是《08-A-AQS与JUC锁体系详解》的配套 B 篇。A 篇讲"AQS 骨架怎么搭、五把锁怎么实现",本篇讲"面试官怎么考、生产上怎么炸、炸了怎么救 "。高频题按知识依赖链一问一答连贯展开 (每题: 30 秒电梯版 → 深挖版 → 追问链),事故按场景 → 根因 → 预防 → 解决 → 一句话教训五段式复盘。原理细节标注"见 A 篇 x.x 节"。
📑 目录
- 02-02-B-AQS与JUC锁面试与生产事故实战
-
- 一、高频面试题精讲(连贯问答链)
-
- [链条①:AQS 三连问](#链条①:AQS 三连问)
- [链条②:ReentrantLock 三连问](#链条②:ReentrantLock 三连问)
- 链条③:读写锁与协作工具四连问
- 二、生产事故案例集(五段式复盘)
-
- [事故一:tryLock 成功后的代码抛异常,锁没释放拖死对账服务](#事故一:tryLock 成功后的代码抛异常,锁没释放拖死对账服务)
- 事故二:读写锁里调外部接口,写线程饿死半小时(临界区过长)
- [事故三:CountDownLatch 没带超时,下游挂了自己跟着挂(雪崩)](#事故三:CountDownLatch 没带超时,下游挂了自己跟着挂(雪崩))
- [事故四:StampedLock 方法重入,线程死锁且 jstack 看不出所以然](#事故四:StampedLock 方法重入,线程死锁且 jstack 看不出所以然)
- 三、事故速查表
- 四、面试答题万能框架
- [五、与 A 篇的知识点映射](#五、与 A 篇的知识点映射)
一、高频面试题精讲(连贯问答链)
链条①:AQS 三连问
Q1:AQS 是什么?核心原理讲一下?
** 30 秒电梯版**
AQS = AbstractQueuedSynchronizer,JUC 锁的公共骨架(见 A 篇第一、二章)。核心就三样:
- 一个 volatile int state:资源状态,含义由子类定义(ReentrantLock=重入次数、Semaphore=许可数、CountDownLatch=倒计时);
- 一个 CLH 变体的 FIFO 双向队列:抢不到资源的线程包成 Node 排队、挂起;
- 模板方法 :排队/挂起/唤醒的脏活 AQS 全包,子类只重写
tryAcquire/tryRelease(独占)或tryAcquireShared/tryReleaseShared(共享)------只回答"怎么算抢到资源"。
一句话总结 :AQS 管排队,子类管抢座;state 用 CAS 改,队列用 park/unpark 管------ReentrantLock、Semaphore、CountDownLatch、线程池 Worker 全是它的子类。
** 深挖版**
| 要点 | 说明 |
|---|---|
| acquire 完整链路 | tryAcquire → 失败 addWaiter(CAS 入队尾)→ acquireQueued 自旋:前驱是 head 就再抢 → 抢不到且前驱 SIGNAL → park |
| 防丢失唤醒 | park 前必须确认前驱 waitStatus=SIGNAL("我走时叫你"的责任书)------没有承诺就不能睡 |
| 独占 vs 共享 | 独占唤醒 head.next 一个;共享拿到资源后传播(PROPAGATE)唤醒一串------读锁连环放行、latch 归零叫醒所有人的原理 |
** 追问链**:state 在不同锁里分别是什么?→ Q2
Q2:AQS 的 state 在 ReentrantLock、Semaphore、CountDownLatch 里分别代表什么?
** 30 秒电梯版**
state 是变色龙,看懂它就看懂了那把锁(见 A 篇 8.2 第二条):
| 锁 | state 含义 | 抢到资源的条件 |
|---|---|---|
| ReentrantLock | 重入次数(0=无人持锁) | CAS 0→1;自己持锁则 +1(可重入) |
| Semaphore | 剩余许可数 | CAS 减 1 够减就拿到 |
| CountDownLatch | 剩余计数 | state==0 时 await 放行(共享) |
| ReadWriteLock | 高 16 位读锁计数 + 低 16 位写锁重入数 | 一个 int 管两把锁 |
| 线程池 Worker | 线程状态(-1 初始化/0 空闲/1 执行中) | 复用 AQS 防止运行中的线程被中断 |
一句话总结 :同一副骨架,换个 state 语义就是一把新锁------这就是模板方法模式的威力。
** 深挖版**
| 要点 | 说明 |
|---|---|
| 为什么 state 是 volatile | 多线程读写要立即可见;修改靠 CAS 保原子------volatile 管看见,CAS 管独占(07-A 篇 Q4 的组合应用) |
| RRWL 的位运算 | 读锁 +1 是 state + (1<<16),写锁 +1 是 state + 1------高低位互不干扰 |
| Worker 为什么继承 AQS | 不是为了锁,是借"不可重入的独占锁"语义:正在跑任务的线程拿不到 Worker 锁,shutdown 时就能区分"空闲线程可中断、忙碌线程不打扰" |
** 追问链**:CLH 队列里的线程怎么被唤醒?→ Q3
Q3:AQS 为什么用 CLH 队列的变体?和原版 CLH 锁什么区别?
** 30 秒电梯版**
原版 CLH 是自旋锁:线程在前驱节点的状态上自旋(前驱释放了我就能看到)。AQS 的变体改了两点:
- 不自旋改挂起 :长时间自旋烧 CPU,AQS 用
LockSupport.park挂起、前驱释放时unpark精确唤醒------适合长时间等待的场景; - 加了 next 指针 :原版 CLH 只有 prev(自旋看前驱就够),AQS 要主动唤醒后继,必须能从 head 找到 next------所以变成双向链表。
一句话总结 :CLH 的排队思想(FIFO 公平性)+ park/unpark 的挂起唤醒(省 CPU)+ 双向指针(能叫醒下家)= AQS 队列。
** 深挖版**
| 要点 | 说明 |
|---|---|
| 为什么不用 LockSupport 直接单线程唤醒 | 队列要处理取消(超时/中断的 CANCELLED 节点)、要保证 FIFO、要防惊群------队列是秩序,park 是开关 |
| 入队为什么 CAS 尾插 | 多线程同时入队,CAS tail 失败就自旋重读再插------无锁入队,AQS 全程几乎不用 synchronized |
| 唤醒为什么只叫 head.next | 独占资源只有一份,叫多了白抢一轮再睡回去(惊群)------精确唤醒省 CPU |
链条②:ReentrantLock 三连问
Q4:ReentrantLock 和 synchronized 的区别?怎么选?
** 30 秒电梯版**
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 层面 | JVM 内置(Monitor+锁升级) | JDK 类库(AQS) |
| 释放 | 自动(异常也释放) | 手动 unlock,必须 finally |
| 可中断 | ❌ | ✅ lockInterruptibly |
| 超时放弃 | ❌ | ✅ tryLock(timeout) |
| 公平可选 | ❌ 只有非公平 | ✅ 构造参数选 |
| 条件队列 | 1 个 WaitSet | 多个 Condition 精确唤醒 |
| 性能 | JDK 6 后相当 | 相当 |
选型 :默认 synchronized (简单、不会忘释放、JVM 持续优化);需要可中断/超时/公平/多条件才上 ReentrantLock。
一句话总结 :synchronized 是自动挡,ReentrantLock 是手动挡------功能多但责任大,忘 unlock 就是生产事故(本篇事故一)。
** 深挖版**
| 要点 | 说明 |
|---|---|
| tryLock 是防死锁利器 | 拿不到就放弃/退避重试,打破"持有并等待"这个死锁必要条件 |
| 底层差异一句话 | synchronized:对象头 Mark Word + Monitor + 锁升级;ReentrantLock:AQS state + CLH 队列------一个靠 JVM,一个靠类库 |
| 虚拟线程时代 | JDK 21 虚拟线程遇 synchronized 会 pinning(钉住载体线程),高并发 IO 场景优先 ReentrantLock(11-A 篇) |
** 追问链**:公平锁和非公平锁差在哪?→ Q5
Q5:公平锁和非公平锁的区别?为什么默认非公平?
** 30 秒电梯版**
源码上就差一行 (见 A 篇 3.2):公平锁的 tryAcquire 多了 hasQueuedPredecessors() 判断------队列里有人排在我前面,我就不抢。非公平锁新来的线程直接 CAS 抢一把,抢不到才排队(插队)。
默认非公平的原因------吞吐 :锁刚释放的瞬间,队头线程还在"被唤醒的路上"(park→unpark→调度要几微秒),新线程直接接手干活------锁不空转、CPU 不切换。代价是队头可能被反复插队(实践中饿死概率极低)。
一句话总结 :非公平用"允许插队"换吞吐,公平用"严格排队"换不饥饿------99% 场景默认非公平就对了。
** 深挖版**
| 要点 | 说明 |
|---|---|
| 什么时候必须公平 | 任务调度要求先来先服务、或队头等待时间有 SLA 约束------明确需求才开,别为了"听起来正确"牺牲吞吐 |
| 非公平的两次插队点 | ① lock() 上来直接 CAS;② 入队后自旋时前驱是 head 还会再 tryAcquire 一次 |
| 对比 synchronized | synchronized 只有非公平------想要公平语义只能 ReentrantLock(true) |
** 追问链**:Condition 和 wait/notify 什么区别?→ Q6
Q6:Condition 的 await/signal 原理?和 wait/notify 什么区别?
** 30 秒电梯版**
核心区别:一把锁可以建多个 Condition,各自一个等待队列,精确唤醒(见 A 篇第四章)。wait/notify 只有一个 WaitSet,notify 随机唤醒------可能叫醒"不该醒的"(生产者叫醒生产者)。
await/signal 的队列迁移:
await():释放锁 (state 减到 0)→ 包成 Node 进条件队列(单链表)→ park;signal():把条件队列头节点转移到 AQS 同步队列(队尾);- 持锁线程 unlock → 唤醒同步队列队头 → 它重新 tryAcquire,抢到锁后 await 才返回。
一句话总结 :await = 还锁去小房间睡,signal = 把你搬回大厅排队------醒来还要重新抢锁,所以 await 必须配 while 防虚假唤醒。
** 深挖版**
| 要点 | 说明 |
|---|---|
| 经典应用 | ArrayBlockingQueue 的 notFull/notEmpty 双 Condition:满了只让生产者睡 notFull,有货只叫醒 notEmpty 里的消费者------零无效唤醒 |
| 为什么必须 while | signal 后到真正拿到锁之间,条件可能又被别人改了(两个消费者被 signal,一个拿完货另一个醒来队列又空了)------醒来必须重新检查 |
| signal vs signalAll | signal 叫一个(够用就 signal,省惊群);signalAll 全搬到同步队列(条件复杂时用,代价是竞争) |
链条③:读写锁与协作工具四连问
Q7:ReadWriteLock 的原理?什么是锁降级?为什么不支持锁升级?
** 30 秒电梯版**
原理 :一个 state 劈两半------高 16 位记读锁持有数,低 16 位记写锁重入数(见 A 篇 5.1)。规则:读读共享、读写互斥、写写互斥。
锁降级(支持) :持写锁 → 加读锁 → 释放写锁。用途:写完数据后"平滑过渡"到读锁,保证自己接下来读到的还是刚写的值(中间不会被其他写线程插队)。
锁升级(不支持) :持读锁直接抢写锁 = 死锁。原因:读读不互斥,线程 A、B 都持读锁,A 等 B 放读锁才能拿写锁,B 等 A 放------互相等,死局。
一句话总结 :降级是"写完顺手拿着读",升级是"读着读着想改"------前者安全后者死锁,所以 RRWL 只开降级的门。
** 深挖版**
| 要点 | 说明 |
|---|---|
| 写锁饥饿 | 非公平模式下读请求源源不断,写线程可能长期抢不到------RRWL 用 hasQueuedPredecessors 缓解(有写排队时新读请求也排队) |
| 读锁为什么不能单独 Condition | 读锁是共享的,Condition 依赖独占持有------只有写锁能 newCondition |
| 适用边界 | 读远多于写、临界区较长(读操作有实际耗时)才划算;临界区极短时锁本身的开销可能超过收益 |
** 追问链**:读还能更快吗?→ Q8
Q8:StampedLock 的乐观读是什么?和 ReadWriteLock 怎么选?
** 30 秒电梯版**
乐观读 = 不加锁只拿凭证 (见 A 篇 5.2):tryOptimisticRead() 返回一个 stamp(不改 state!),读完字段后 validate(stamp) 校验期间有没有发生过写------没有就直接用(零锁开销),有就升级成悲观读锁重读。
对比:
| 维度 | ReentrantReadWriteLock | StampedLock |
|---|---|---|
| 读开销 | 共享锁,仍要 CAS 改 state(热点) | 乐观读不改 state,更快 |
| 可重入 | ✅ | ❌ 重入直接死锁 |
| Condition | ✅(写锁) | ❌ |
| 中断 | 支持 | readLock 不可中断(用 readLockInterruptibly) |
选型 :默认 RRWL(稳妥);极致读性能、临界区短、能遵守"不可重入"约束才上 StampedLock。
一句话总结 :乐观读赌"读期间没人写",赌对了零开销,赌错了退回悲观锁------是"读多写极少"场景的天花板。
** 深挖版**
| 要点 | 说明 |
|---|---|
| 使用陷阱 | 乐观读期间字段可能被并发修改(读到撕裂值)------必须先拷到局部变量再 validate,validate 不过就重读 |
| 为什么不可重入 | 没有记录持有线程的机制,重入写锁 = 自己等自己------死锁且无日志,极难排查 |
| 现代替代思路 | 读多写少的配置/缓存场景:volatile 发布不可变对象(CopyOnWrite 思想)往往比任何读写锁都快(07-A 篇 7.3 安全发布) |
** 追问链**:不是锁的"锁"呢?→ Q9
Q9:Semaphore、CountDownLatch、CyclicBarrier 的区别?
** 30 秒电梯版**
| Semaphore | CountDownLatch | CyclicBarrier | |
|---|---|---|---|
| 语义 | 限制同时 N 个 | 等 N 件事完成 | N 个线程互相等齐 |
| 谁等谁 | 没有"等齐",只有"限额" | 一个(组)线程等另外 N 个 | N 个线程互相等 |
| 可重用 | ✅ release 后许可回来 | ❌ 一次性 | ✅ 可 reset 循环用 |
| 底层 | AQS 共享(state 加减) | AQS 共享(state 减到 0) | ReentrantLock + Condition |
| 典型场景 | 接口限流、连接池 | 主线程等 3 个 RPC 全回来 | 多线程分段计算后汇总 |
一句话总结 :Semaphore 管"车位",Latch 管"等事",Barrier 管"等人"------latch 一次性,barrier 可循环。
** 深挖版**
| 要点 | 说明 |
|---|---|
| Semaphore 不是"锁" | 许可可以由别的线程 release(锁只能持有者释放)------所以能做"生产许可/消费许可"分离的场景 |
| latch 的两个军规 | countDown 必须在 finally (任务异常也要减,否则 await 卡死);await 必须带超时(下游挂了别把自己拖死)------本篇事故三 |
| 现代替代 | 并行任务编排优先 CompletableFuture.allOf:能拿返回值、能组合异常、自带超时(orTimeout),比裸 latch 安全(10-A 篇) |
** 追问链**:这些工具出过什么生产事故?→ Q10
Q10:JUC 锁在生产上最容易踩的坑有哪些?
** 30 秒电梯版**
四大高频坑(详见第二章事故集):
- lock() 不配 try-finally:异常路径锁不释放,全服务 BLOCKED(事故一);
- await/tryAcquire 不带超时:下游一挂,自己线程全堆在等待上,线程池耗尽雪崩(事故三);
- 锁了错误粒度的对象:锁 this 但 Service 是多实例、锁 String/Long(缓存池陷阱)------等于没锁(07-B 篇事故三);
- StampedLock 重入:不可重入的锁重入了,死锁且无线程 dump 线索(事故四)。
一句话总结 :JUC 锁给了自由也给了责任------finally、超时、锁对象、重入性,四个检查点一个都不能少。
** 深挖版**
| 要点 | 说明 |
|---|---|
| 死锁四条件与破解 | 互斥/持有并等待/不可剥夺/循环等待------tryLock 超时破"持有并等待",固定加锁顺序破"循环等待" |
| 排查工具链 | jstack 自动检测 Java-level deadlock;BLOCKED 线程找 waiting to lock <0x..> 反查持锁者;async-profiler lock 模式看锁竞争热点(06-A 篇 SOP-4) |
| 评审清单 | 新增锁代码必查:unlock 在 finally?await 有超时?锁对象 final 专用?可重入性确认?临界区够短? |
二、生产事故案例集(五段式复盘)
事故一:tryLock 成功后的代码抛异常,锁没释放拖死对账服务
** 事故场景**
对账服务用 ReentrantLock 保护"查库+核对+写结果"复合操作。某天数据库抖动抛 SQLException 后,所有对账线程陆续 BLOCKED ,jstack 显示几十个线程 waiting to lock <0x000000076b3a2f10>,而持有该锁的线程早已结束。服务假死,只能重启。
** 根因分析**
unlock 不在 finally(Q10 坑 1,07-B 篇事故四同款):
java
if (lock.tryLock()) {
doReconcile(); // 抛了 SQLException
lock.unlock(); // 永远执行不到
}
异常沿栈上抛跳过 unlock,AQS 的 state 永远 ≥1、owner 永远是已退出的线程------后续所有 tryAcquire 失败入队 park,且没人会来 unpark 它们。
️ 预防方案
- 模板固化 :
lock()/tryLock()成功后下一行必须是 try,unlock 必须在 finally------脚手架生成、CR 必查; - 静态扫描 :正则/AST 规则扫
\.lock\(\)与\.unlock\(\)不在同一 try-finally 结构的代码; - 监控兜底:线程 BLOCKED 数量告警 + 锁等待时长指标(Micrometer Timer 包一层)。
** 事故解决**
- 止血:重启;
- 定位 :jstack 找
waiting to lock的锁地址 → 全局搜locked <同地址>无果(持锁线程已退出)→ 确认锁泄漏; - 根治:修复该处 + 全仓扫描出 7 处同款隐患一并修复。
** 一句话教训**
AQS 不会替你收尾------state 是你加上去的,就必须由你在 finally 里减下来。
事故二:读写锁里调外部接口,写线程饿死半小时(临界区过长)
** 事故场景**
商品配置服务用 ReentrantReadWriteLock:读锁保护配置读取(QPS 5000+),写锁保护配置更新(运营后台低频操作)。某次运营改配置,保存按钮转圈 30 分钟无响应;期间读请求正常。重启后恢复,过几天又复现。
** 根因分析**
写锁饥饿 + 临界区里干重活 (Q7 深挖版):读请求源源不断,非公平模式下写线程排队时新读线程还在插队;更要命的是写锁临界区里同步调用了配置中心的推送接口 (那次网络抖动耗时极长)------写锁持有越久,读队列越深,写线程重入的机会越渺茫。读锁不互斥的"优点",反过来成了写锁的坟墓。
️ 预防方案
- 临界区纪律 :锁内只做内存操作,RPC/IO/大计算一律移出临界区(先算好结果,进锁只做赋值);
- 写操作加超时 tryLock(5s),拿不到就报错让运营重试,别无限等;
- 高频读 + 低频写的配置场景,评估改用 volatile 不可变对象整体替换 (CopyOnWrite 思想)------写线程构造新配置对象,一次 volatile 赋值切换,读完全无锁;
- 监控:读写锁等待时间分位数打点。
** 事故解决**
- 止血:重启 + 暂停配置变更;
- 根治:改造为"锁外构建新配置 → 锁内(或直接 volatile)切换引用",推送配置中心改为异步 MQ;
- 验证:压测读 5000 QPS 下写操作 P99 < 50ms。
** 一句话教训**
读写锁的临界区里放一个 RPC------读线程的汪洋大海会把写线程淹死;锁内只干内存活,是读写锁的第一纪律。
事故三:CountDownLatch 没带超时,下游挂了自己跟着挂(雪崩)
** 事故场景**
首页聚合服务用 CountDownLatch(3) 并行调用用户/商品/营销三个服务后 latch.await()(无超时 )。某天营销服务 Full GC 卡死 2 分钟,聚合服务的 Tomcat 线程全部堆在 await 上,线程池耗尽,首页整体 502------一个下游拖死整个入口。
** 根因分析**
await 无超时 + countDown 不在 finally 的双重隐患 (Q9 深挖版):营销服务的任务线程卡住,countDown 迟迟不执行,state 永远不为 0;await 无超时就永远 park。聚合服务没有自己的"求生欲"------把命交给了最慢的下游。
️ 预防方案
- 军规:所有 await/tryAcquire/join 必须带超时,超时走降级(返回缓存/默认值/部分数据);
- countDown 必须在 finally(任务抛异常也要减);
- 下游调用本身要有超时+熔断(Sentinel/Resilience4j),别用 latch 的超时当下游超时------两层都要有;
- 现代写法:CompletableFuture.allOf(...).orTimeout(500, MILLISECONDS)------超时、异常、组合一站式。
** 事故解决**
- 止血:重启聚合服务 + 营销服务限流;
- 根治:await(500ms) + 超时降级返回缓存数据;三个调用全部加 RPC 超时 300ms;
- 验证:故障演练(kill 营销服务),首页 RT 稳定在 600ms 内、返回降级标记。
** 一句话教训**
不带超时的 await 是把服务的命门交给下游------超时+降级不是可选项,是聚合服务的氧气面罩。
事故四:StampedLock 方法重入,线程死锁且 jstack 看不出所以然
** 事故场景**
计价服务用 StampedLock 保护费率表。重构时有人在 calculate()(持写锁)内部调用了同样加写锁的 refreshCache()------上线后调用到该路径的请求全部卡死 ,jstack 显示线程停在 StampedLock 内部,没有 "Found one Java-level deadlock" 提示(不是经典互等死锁),排查半天。
** 根因分析**
StampedLock 不可重入 (Q8 深挖版):写锁的 state 已被自己持有,再次 writeLock() 时 tryAcquire 发现 state 非 0------它不知道持有者就是自己 (没有 owner 记录),于是入队 park,等"自己"释放------自己等自己,死局。且因为不是两线程互等,jstack 的死锁检测器识别不出来。
️ 预防方案
- 选型时确认重入需求:调用链可能重入的场景禁用 StampedLock,用 ReentrantLock/RRWL(可重入);
- 代码结构 :持锁方法拆成"公开入口(加锁)+ 私有实现(无锁)"两层,内部调用一律走无锁实现------从结构上杜绝重入;
- CR 检查点:StampedLock 的类里搜"加锁方法调加锁方法";
- 排查知识沉淀:"线程卡在 StampedLock 且无死锁报告" = 先查重入。
** 事故解决**
- 止血:回滚版本;
- 根治:refreshCache 逻辑内联到 calculate 的锁内(去掉嵌套加锁),并补单测覆盖该路径;
- 验证:并发压测该路径 1 万次无卡死。
** 一句话教训**
StampedLock 快是真快,不认人也是真不认人------它没有 owner 字段,重入就是自己给自己上枷锁,还不上报死锁。
三、事故速查表
| 现象 | 可能根因 | 快速定位 | 根治方案 |
|---|---|---|---|
| 线程全 BLOCKED、持锁线程已退出 | unlock 不在 finally | jstack 找 waiting to lock 地址反查无持有者 | lock 后紧跟 try-finally;全仓扫描 |
| 写操作偶发超长等待、读正常 | 读写锁写饥饿 + 临界区含 IO | 锁等待打点;审查写锁内代码 | IO 移出临界区;tryLock 超时;改 volatile 不可变对象 |
| 下游故障时自己线程池耗尽 | await/tryAcquire 无超时 | 线程 dump 全堆在 await/park | 全部带超时+降级;CompletableFuture.orTimeout |
| 卡在 StampedLock、无死锁报告 | 不可重入锁被重入 | 审查调用链:加锁方法调加锁方法 | 拆公开入口+私有无锁实现;换可重入锁 |
| 两线程互等、jstack 报 deadlock | 加锁顺序不一致 | jstack 死锁环 | 固定全局加锁顺序;tryLock 超时破持有并等待 |
| CPU 高、吞吐低、锁竞争激烈 | 临界区过大/锁粒度过粗 | async-profiler lock 火焰图 | 缩小临界区;锁分段(ConcurrentHashMap 思想);无锁化 |
| Semaphore 许可越用越少 | release 不在 finally/重复 acquire | 许可数监控打点 | try-finally release;acquire/release 配对审查 |
四、面试答题万能框架
#mermaid-svg-yYYA4GKiqlXXjUef{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yYYA4GKiqlXXjUef .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yYYA4GKiqlXXjUef .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yYYA4GKiqlXXjUef .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-yYYA4GKiqlXXjUef .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-yYYA4GKiqlXXjUef .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yYYA4GKiqlXXjUef .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yYYA4GKiqlXXjUef .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yYYA4GKiqlXXjUef .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yYYA4GKiqlXXjUef .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yYYA4GKiqlXXjUef .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yYYA4GKiqlXXjUef .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-yYYA4GKiqlXXjUef .marker.cross{stroke:#0b0b0b;}#mermaid-svg-yYYA4GKiqlXXjUef svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yYYA4GKiqlXXjUef p{margin:0;}#mermaid-svg-yYYA4GKiqlXXjUef .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yYYA4GKiqlXXjUef .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-yYYA4GKiqlXXjUef .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-yYYA4GKiqlXXjUef .cluster-label span p{background-color:transparent;}#mermaid-svg-yYYA4GKiqlXXjUef .label text,#mermaid-svg-yYYA4GKiqlXXjUef span{fill:#333;color:#333;}#mermaid-svg-yYYA4GKiqlXXjUef .node rect,#mermaid-svg-yYYA4GKiqlXXjUef .node circle,#mermaid-svg-yYYA4GKiqlXXjUef .node ellipse,#mermaid-svg-yYYA4GKiqlXXjUef .node polygon,#mermaid-svg-yYYA4GKiqlXXjUef .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-yYYA4GKiqlXXjUef .rough-node .label text,#mermaid-svg-yYYA4GKiqlXXjUef .node .label text,#mermaid-svg-yYYA4GKiqlXXjUef .image-shape .label,#mermaid-svg-yYYA4GKiqlXXjUef .icon-shape .label{text-anchor:middle;}#mermaid-svg-yYYA4GKiqlXXjUef .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yYYA4GKiqlXXjUef .rough-node .label,#mermaid-svg-yYYA4GKiqlXXjUef .node .label,#mermaid-svg-yYYA4GKiqlXXjUef .image-shape .label,#mermaid-svg-yYYA4GKiqlXXjUef .icon-shape .label{text-align:center;}#mermaid-svg-yYYA4GKiqlXXjUef .node.clickable{cursor:pointer;}#mermaid-svg-yYYA4GKiqlXXjUef .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-yYYA4GKiqlXXjUef .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-yYYA4GKiqlXXjUef .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-yYYA4GKiqlXXjUef .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-yYYA4GKiqlXXjUef .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-yYYA4GKiqlXXjUef .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-yYYA4GKiqlXXjUef .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-yYYA4GKiqlXXjUef .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-yYYA4GKiqlXXjUef .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-yYYA4GKiqlXXjUef .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-yYYA4GKiqlXXjUef .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-yYYA4GKiqlXXjUef div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yYYA4GKiqlXXjUef .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yYYA4GKiqlXXjUef rect.text{fill:none;stroke-width:0;}#mermaid-svg-yYYA4GKiqlXXjUef .icon-shape,#mermaid-svg-yYYA4GKiqlXXjUef .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-yYYA4GKiqlXXjUef .icon-shape p,#mermaid-svg-yYYA4GKiqlXXjUef .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-yYYA4GKiqlXXjUef .icon-shape .label rect,#mermaid-svg-yYYA4GKiqlXXjUef .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-yYYA4GKiqlXXjUef .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yYYA4GKiqlXXjUef .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yYYA4GKiqlXXjUef :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 被问AQS/JUC锁
① 先给骨架 state+CLH队列+模板方法
30秒讲清AQS是什么
② 按追问深挖 原理线:acquire流程/
SIGNAL防丢唤醒/传播
源码线:公平差一行hasQueue
dPredecessors
对比线:vs synchroniz
ed/RRWL vs StampedLock
③ 落到生产视角 finally军规/超时军规/
临界区纪律 锁对象粒度/重入性确认
④ 用事故收尾 锁泄漏假死/
写饥饿/latch雪崩 有画面感的案例胜过背书
加分技巧:
- 谈 AQS 主动讲"SIGNAL 是叫醒责任书,防丢失唤醒"------90% 候选人只会背"state+队列";
- 谈公平锁主动说"源码就差一行 hasQueuedPredecessors,非公平省的是释放→唤醒→切换的空窗"------源码级理解;
- 谈读写锁主动带"降级支持、升级死锁的原因(读读不互斥互等)"------推理能力展示;
- 谈 StampedLock 主动提"不可重入且 jstack 不报死锁"------生产味十足;
- 被问"遇到过什么锁问题",用事故二(写饥饿)或事故三(latch 雪崩)------都带"为什么会这样"的机制解释,不是纯故事。
五、与 A 篇的知识点映射
| 本篇题目/事故 | A 篇《AQS与JUC锁体系详解》对应章节 |
|---|---|
| Q1 AQS 原理 | 一、公共骨架 + 2.2 acquire 流程 |
| Q2 state 含义 | 二、2.1 数据结构 + 8.2 第二条 |
| Q3 CLH 变体 | 2.1 Node/waitStatus + 2.2 设计细节 |
| Q4 vs synchronized | 三、ReentrantLock(对比 07-A 篇第五章) |
| Q5 公平/非公平 | 3.2 |
| Q6 Condition | 四、两个队列的迁移 |
| Q7 读写锁/降级 | 5.1 |
| Q8 StampedLock | 5.2 |
| Q9 三工具对比 | 六、6.3 对比表 |
| Q10 生产坑 | 七、实战军规 + 8.2 第十一条 |
| 事故一 锁泄漏 | 3.3 使用军规 |
| 事故二 写饥饿 | 5.1 锁降级/饥饿 |
| 事故三 latch 雪崩 | 7.2 两个军规 |
| 事故四 StampedLock 重入 | 5.2 对比表"不可重入" |
📌 结语 :JUC 锁面试题的尽头是"骨架意识 "------五把锁一副骨架(AQS),看懂 state 的语义变换就看懂了全部;生产事故的尽头是"纪律意识"------finally、超时、临界区、重入性,四条纪律挡住 90% 的锁事故。
📌 配套阅读:
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!