AQS:公平/非公平、自旋与阻塞(park)的取舍、适用场景与常见坑

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 相关栈(且业务线程没在做 IO)

4. 常见坑:公平/非公平 + 自旋在业务里怎么出事故

  • 坑 1:把慢操作放进锁里

    • 例如锁内调用 RPC/写磁盘/慢 SQL
    • 结果:锁队列堆积,线程大量 parking,RT 飙升
  • 坑 2:以为公平锁能解决一切

    • 公平只是减少插队,不会让临界区变短
    • 临界区长时,公平锁反而更"平均地慢"
  • 坑 3:高并发下用一个全局锁保护大对象

    • 结果:吞吐被锁串行化
    • 优先考虑:拆分锁粒度、分段、无锁/原子类、读写分离等

5. 线上排查清单(非常实用)

当你怀疑锁竞争时:

  • 先看指标:RT、QPS、线程数、CPU、GC
  • jstack 抓 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 空转与延迟抖动。

相关推荐
yueqc12 小时前
垃圾回收器(二):G1
jvm·gc·g1
爱丽_2 小时前
AQS 的 CLH 同步队列:入队/出队、park/unpark 与“公平性”从哪来
java·开发语言·jvm
Barkamin2 小时前
JVM核心简单介绍
jvm
Rick19933 小时前
JVM 高频10问
jvm
皙然3 小时前
AQS模型详解:Java并发的核心同步框架(从原理到实战)
java·开发语言·jvm
愤豆3 小时前
08-Java语言核心-JVM原理-垃圾收集详解
java·开发语言·jvm
再卷也是菜4 小时前
第一章、线性代数(1)矩阵乘法
线性代数·矩阵
南境十里·墨染春水4 小时前
C++传记 this指针 及区分静态非静态成员(面向对象)
开发语言·jvm·c++·笔记
DJ斯特拉4 小时前
JUC基础
java·jvm·juc