的取舍、适用场景与常见坑)
AQS 相关面试追问经常不问“原理”而问“取舍”为什么默认非公平什么场景要公平自旋什么时候是赚的什么时候是亏的这篇把这些问题讲透。你必须记住的 3 句话面试直出公平/非公平的本质是“能不能插队走快路径”不是“有没有队列”。自旋的收益来自“避免 park/unpark 的调度成本”但临界区一长就会变成 CPU 空转。线上锁问题先看现象parking多排队等待CPU 高但吞吐不涨可能 CAS 热点 自旋。0. 快速选型表背下来就够用目标/约束更推荐理由吞吐优先大多数业务非公平锁快路径更容易命中减少排队与调度开销等待顺序可预测/更强公平诉求公平锁禁止插队减少短期饥饿临界区很短纯内存少量自旋可接受可能比阻塞/唤醒更划算临界区包含 IO/慢 SQL/RPC避免自旋让线程阻塞自旋会放大 CPU 浪费与抖动1. 公平 vs 非公平差的不是“有没有队列”而是“能不能插队”1.1 公平锁Fair核心规则只要队列里有人排在你前面你就不允许直接抢收益更可预测减少饥饿代价更频繁的排队/唤醒吞吐更低1.2 非公平锁Nonfair核心规则新来的线程可以先 CAS 抢一次 state插队失败再入队一个面试加分点在ReentrantLock中非公平实现会先做一次compareAndSetState(0, 1)的快路径只有失败才走 AQS 入队逻辑。收益快路径命中率更高吞吐通常更高代价短期不公平极端情况下可能出现“某些线程总抢不到”但一般会靠调度逐步缓解关于“饥饿”的边界你可以这么答非公平锁允许插队所以“理论上”可能饥饿但 JVM/OS 调度通常会让等待线程获得执行机会实践中更多是“短期不公平”。2. 自旋 vs 阻塞AQS 的策略是“先试、再睡”AQS 的典型行为可以概括为快速 CAS 尝试获取快路径失败 - 入队入队后会在合适时机再尝试一次通常是成为 head 后继时再失败 -park阻塞为什么这么设计阻塞/唤醒是系统调用级别的成本切换、调度、缓存失效如果锁很快释放少量自旋可能更划算3. 怎么判断“自旋赚不赚”经验判断临界区很短几十纳秒~微秒级纯内存操作适合少量自旋避免 park/unpark临界区较长IO、RPC、慢 SQL、日志同步写、磁盘自旋大概率亏CPU 空转 还会放大系统抖动你可以用一句话回答自旋适合“短临界区 低竞争”否则应该让线程阻塞把 CPU 让给能干活的线程。线上识别自旋/竞争过热的常见信号CPU 占用高、上下文切换也高但 QPS/TPS 不涨大量线程反复出现在 acquire 相关栈且业务线程没在做 IO4. 常见坑公平/非公平 自旋在业务里怎么出事故坑 1把慢操作放进锁里例如锁内调用 RPC/写磁盘/慢 SQL结果锁队列堆积线程大量parkingRT 飙升坑 2以为公平锁能解决一切公平只是减少插队不会让临界区变短临界区长时公平锁反而更“平均地慢”坑 3高并发下用一个全局锁保护大对象结果吞吐被锁串行化优先考虑拆分锁粒度、分段、无锁/原子类、读写分离等5. 线上排查清单非常实用当你怀疑锁竞争时先看指标RT、QPS、线程数、CPU、GCjstack 抓 3 次间隔几秒大量线程在AbstractQueuedSynchronizer.acquire/LockSupport.park且卡在同一把锁的获取路径结合代码定位锁范围锁内是否包含 IO/慢 SQL/外部调用你还可以补一个更“面试像实战”的说法先定位“谁在持有锁”抓 3 次 jstack找出持锁线程的业务栈看它在锁内到底干了什么。定位到后常见修复手段缩小锁范围拆分锁按 key 分段读多写少用读写锁或 CopyOnWrite谨慎计数聚合用LongAdder替代AtomicLong热点写6. 自测清单你要能顺口讲出来Q为什么ReentrantLock默认非公平A允许插队提升快路径命中率与吞吐一般业务更看重性能。Q什么时候选公平锁A需要更可预测的等待顺序、避免某些线程长时间拿不到锁例如特定调度场景、资源分配更敏感。Q自旋一定浪费 CPU 吗A不一定。临界区很短且竞争不激烈时自旋减少 park/unpark 成本反而更快但竞争激烈/临界区长会让 CPU 空转。Q公平锁是不是“严格 FIFO”A它的目标是“减少插队”不保证严格 FIFO 获得锁被唤醒后仍要靠 CAS 再竞争。7. 30 秒背诵稿公平与非公平的核心差异是“能否插队走快路径”公平锁发现队列有前驱就排队非公平锁先 CAS 抢一次提高吞吐。AQS 获取失败后入队head 后继会再竞争仍失败才 park 阻塞自旋适合短临界区、低竞争以减少 park/unpark 成本但临界区包含 IO/慢 SQL/RPC 时会造成 CPU 空转与延迟抖动。