← 每日面试题 学习看板

AQS 自研同步器接入业务限流:如何设计 state 语义并避免唤醒丢失与惊群?

面试题/00java/02Java 并发编程/09编程实践与应用场景/AQS 自研同步器接入业务限流:如何设计 state 语义并避免唤醒丢失与惊群?.md

面试记录 2026-10-20

题目:AQS 自研同步器接入业务限流:如何设计 state 语义并避免唤醒丢失与惊群?

难度:high

标签:java/aqs, java/concurrency

评分:⭐⭐☆☆☆(2/5)

我的回答

AQS自研同步器接入业务限流,要进行限流,state要控制在初始化的时候设置的就是单位时间内的限流量,当限流后其实有两个处理方案,一个是返回异常进行阻止后续请求实现限流;另一个是放入队列中等待前置任务完成,然后进行唤醒队列中的任务,继续拿出一批进行执行;

前者会出现丢失,后者可能会让任务产生堆积,长时间执行不完;

前者是常规的限流业务处理方案,只要给出好的反馈就好,后者有一点类似排队任务的概念,这里需要把控下,具体要能有多少任务被保留无限大队列会让jvm发生oom无法使用;

为了防止前置任务执行完成,后续任务全部唤醒,可以使用公平锁的模式,从队列中拿出最多state限流配置的任务数去执行;

点评

优点:意识到 state 承载"限流额度"语义;区分了"快速失败"与"排队等待"两种策略;知道要有界队列防 OOM;知道用公平性控制唤醒顺序

不足:① state 语义没闭环——限流器 state 应该是"当前可用许可数",需要 tryAcquireShared 里 CAS 递减、释放时 CAS 递增,且必须处理"许可不够时返回负值让 AQS 入队";② "唤醒丢失"的原因说错了——不是"前置完成就丢失",而是 release 时未在临界区检查/未调用 unpark,或 signal 先于 await 导致条件丢失,AQS 内部靠 waitStatus 的 SIGNAL 状态避免;③ 惊群:正确做法是共享模式下只唤醒必要数量(setHeadAndPropagate 按剩余许可传播),而不是"用公平锁";④ 未提超时(tryAcquireSharedNanos)、许可的时间窗口重置(需要额外 nextFreeTicketMicros,AQS 的 state 单变量不够用)

补充:单变量 state 只够做"并发数信号量";要做 QPS 限流得叠加时间窗口(如令牌桶的 nextFreeTicket),或者直接上 Guava RateLimiter/Redisson RRateLimiter

完整讲解

第一部分:知识重点剖析

核心考点定位:能否把限流语义映射到 AQS 的 state + 共享模式,并说清唤醒丢失/惊群的真正成因。

知识点拆解:

  1. state 语义设计:state = 当前可用许可数;tryAcquireShared(arg) 循环 CAS state - arg,成功返回剩余值(≥0),失败返回负值让 AQS 入队;tryReleaseShared CAS state + arg 并返回 true 触发传播
  2. 唤醒丢失的真正成因(AQS 之外的 Condition 场景):signal() 必须在持有锁时调用;先 signal 后 await 会导致信号丢失;AQS 队列侧则靠 waitStatus=SIGNAL 标记 + unparkSuccessor 保证不漏唤醒
  3. 惊群规避:共享模式释放时 setHeadAndPropagate(node, r) 只在 r > 0 或 PROPAGATE 状态下继续唤醒后继,按剩余许可传播,不是全量 unpark
  4. 公平性的作用:hasQueuedPredecessors() 只保证入队顺序,不解决惊群;高争用下限流器通常选非公平(允许抢占许可)
  5. AQS 的边界:单 state 只能表达"瞬时并发数";要做"每秒钟 N 个"必须引入时间维度(令牌桶 nextFreeTicketMicros、滑动窗口),这已经超出 AQS 单变量能力
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. Semaphore 的公平模式为什么慢?
  1. AQS 实现限流,许可怎么"按时间恢复"?
  1. 限流器被唤醒的线程太多导致 CPU 抖动,怎么查?

第三部分:面试者参考回答(三步法)

【一句话结论】

面试官您好,如果只是"同时最多 N 个",AQS 的共享模式能直接做;但要做"每秒 N 个",单靠 state 不够,得引入时间维度。

【三点展开】

第一,state 设计成可用许可数。acquire 走 tryAcquireShared,循环 CAS 把 state 减一,成功返回剩余值,失败返回负数让 AQS 把线程挂到队列;release 走 tryReleaseShared 把 state 加回去并触发传播。这样"排队等待"和"快速失败"就变成了两个入口:acquire 阻塞,tryAcquire 直接返回 false。

第二,唤醒丢失不是"前置完成后丢了",而是释放路径没做对:共享模式释放后必须让 setHeadAndPropagate 按剩余许可继续唤醒后继;如果是 Condition 场景,signal 必须在持锁时调用,且要防止先 signal 后 await。AQS 队列侧靠 waitStatus 的 SIGNAL 标记兜底,漏唤醒基本都出在自定义 tryReleaseShared 返回值写错。

第三,惊群要靠"按需传播"而不是公平锁。公平锁只保证入队顺序,不减少唤醒数量;真正的做法是只在有许可时往后传播,配合有界队列和拒绝策略,避免一次放进来几千个线程又全部抢不到。

【一句边界】

当然,要做 QPS 限流 AQS 就不够用了——state 是单变量,表达不了时间窗口;生产上直接用 Guava RateLimiter、Sentinel 或 Redis 令牌桶,别自己造。