02-02-B-AQS与JUC锁面试与生产事故实战

02-02-B-AQS与JUC锁面试与生产事故实战

关键词:AQS 面试题 · ReentrantLock · 公平锁 · 读写锁 · StampedLock · Semaphore 限流 · CountDownLatch · 死锁复盘

📌 导读 :这是《08-A-AQS与JUC锁体系详解》的配套 B 篇。A 篇讲"AQS 骨架怎么搭、五把锁怎么实现",本篇讲"面试官怎么考、生产上怎么炸、炸了怎么救 "。高频题按知识依赖链一问一答连贯展开 (每题: 30 秒电梯版 → 深挖版 → 追问链),事故按场景 → 根因 → 预防 → 解决 → 一句话教训五段式复盘。原理细节标注"见 A 篇 x.x 节"。


📑 目录


一、高频面试题精讲(连贯问答链)

链条①:AQS 三连问

Q1:AQS 是什么?核心原理讲一下?

** 30 秒电梯版**

AQS = AbstractQueuedSynchronizer,JUC 锁的公共骨架(见 A 篇第一、二章)。核心就三样:

  1. 一个 volatile int state:资源状态,含义由子类定义(ReentrantLock=重入次数、Semaphore=许可数、CountDownLatch=倒计时);
  2. 一个 CLH 变体的 FIFO 双向队列:抢不到资源的线程包成 Node 排队、挂起;
  3. 模板方法 :排队/挂起/唤醒的脏活 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 的变体改了两点:

  1. 不自旋改挂起 :长时间自旋烧 CPU,AQS 用 LockSupport.park 挂起、前驱释放时 unpark 精确唤醒------适合长时间等待的场景
  2. 加了 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 的队列迁移

  1. await()释放锁 (state 减到 0)→ 包成 Node 进条件队列(单链表)→ park;
  2. signal():把条件队列头节点转移到 AQS 同步队列(队尾);
  3. 持锁线程 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 秒电梯版**

四大高频坑(详见第二章事故集):

  1. lock() 不配 try-finally:异常路径锁不释放,全服务 BLOCKED(事故一);
  2. await/tryAcquire 不带超时:下游一挂,自己线程全堆在等待上,线程池耗尽雪崩(事故三);
  3. 锁了错误粒度的对象:锁 this 但 Service 是多实例、锁 String/Long(缓存池陷阱)------等于没锁(07-B 篇事故三);
  4. 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 它们

️ 预防方案

  1. 模板固化lock() / tryLock() 成功后下一行必须是 try,unlock 必须在 finally------脚手架生成、CR 必查;
  2. 静态扫描 :正则/AST 规则扫 \.lock\(\)\.unlock\(\) 不在同一 try-finally 结构的代码;
  3. 监控兜底:线程 BLOCKED 数量告警 + 锁等待时长指标(Micrometer Timer 包一层)。

** 事故解决**

  • 止血:重启;
  • 定位 :jstack 找 waiting to lock 的锁地址 → 全局搜 locked <同地址> 无果(持锁线程已退出)→ 确认锁泄漏;
  • 根治:修复该处 + 全仓扫描出 7 处同款隐患一并修复。

** 一句话教训**

AQS 不会替你收尾------state 是你加上去的,就必须由你在 finally 里减下来


事故二:读写锁里调外部接口,写线程饿死半小时(临界区过长)

** 事故场景**

商品配置服务用 ReentrantReadWriteLock:读锁保护配置读取(QPS 5000+),写锁保护配置更新(运营后台低频操作)。某次运营改配置,保存按钮转圈 30 分钟无响应;期间读请求正常。重启后恢复,过几天又复现。

** 根因分析**

写锁饥饿 + 临界区里干重活 (Q7 深挖版):读请求源源不断,非公平模式下写线程排队时新读线程还在插队;更要命的是写锁临界区里同步调用了配置中心的推送接口 (那次网络抖动耗时极长)------写锁持有越久,读队列越深,写线程重入的机会越渺茫。读锁不互斥的"优点",反过来成了写锁的坟墓

️ 预防方案

  1. 临界区纪律 :锁内只做内存操作,RPC/IO/大计算一律移出临界区(先算好结果,进锁只做赋值);
  2. 写操作加超时 tryLock(5s),拿不到就报错让运营重试,别无限等;
  3. 高频读 + 低频写的配置场景,评估改用 volatile 不可变对象整体替换 (CopyOnWrite 思想)------写线程构造新配置对象,一次 volatile 赋值切换,读完全无锁
  4. 监控:读写锁等待时间分位数打点。

** 事故解决**

  • 止血:重启 + 暂停配置变更;
  • 根治:改造为"锁外构建新配置 → 锁内(或直接 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。聚合服务没有自己的"求生欲"------把命交给了最慢的下游

️ 预防方案

  1. 军规:所有 await/tryAcquire/join 必须带超时,超时走降级(返回缓存/默认值/部分数据);
  2. countDown 必须在 finally(任务抛异常也要减);
  3. 下游调用本身要有超时+熔断(Sentinel/Resilience4j),别用 latch 的超时当下游超时------两层都要有;
  4. 现代写法: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 的死锁检测器识别不出来。

️ 预防方案

  1. 选型时确认重入需求:调用链可能重入的场景禁用 StampedLock,用 ReentrantLock/RRWL(可重入);
  2. 代码结构 :持锁方法拆成"公开入口(加锁)+ 私有实现(无锁)"两层,内部调用一律走无锁实现------从结构上杜绝重入
  3. CR 检查点:StampedLock 的类里搜"加锁方法调加锁方法";
  4. 排查知识沉淀:"线程卡在 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% 的锁事故。
📌 配套阅读

上一篇:《02-01-A-JMM与volatile-synchronized详解.md》

 A 篇:《02-02-A-AQS与JUC锁体系详解.md》

下一篇:《02-03-A-线程池详解.md》

如果这篇文章对你有帮助,欢迎点赞、收藏、关注!

相关推荐
音符犹如代码1 小时前
Kafka 接入 AI 的三条路线:MCP 提案、会话记忆与实时上下文
java·大数据·ai·kafka
vx_BS813301 小时前
【项目编号:project51629】Spring Boot 乡村自来水缴费系统:把抄表、水费生成与在线缴费做成一条数字化闭环
java·spring boot·eclipse·tomcat·maven
qq_269506751 小时前
第29课-Servlet基础
java
毅炼1 小时前
不写多语言 SDK,怎么让 Python、Go、Node 服务接入注册中心?
java·后端·系统架构·gateway
2501_937860942 小时前
上篇:网络编程基础与UDP套接字编程
java·网络·计算机网络
泡海椒2 小时前
jquick-pdf 表格行列尺寸控制实战:单元格合并的可行边界
java·开发语言·pdf
Wang's Blog2 小时前
Java 项目实战: 外卖平台-后台系统登录功能开发
java·服务器·redis
海鸥-w2 小时前
spingboot定义一个全局异常处理器
java
曹牧2 小时前
Jackson 反序列化字段名不匹配
java