题目: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 + 共享模式,并说清唤醒丢失/惊群的真正成因。
知识点拆解:
state = 当前可用许可数;tryAcquireShared(arg) 循环 CAS state - arg,成功返回剩余值(≥0),失败返回负值让 AQS 入队;tryReleaseShared CAS state + arg 并返回 true 触发传播signal() 必须在持有锁时调用;先 signal 后 await 会导致信号丢失;AQS 队列侧则靠 waitStatus=SIGNAL 标记 + unparkSuccessor 保证不漏唤醒setHeadAndPropagate(node, r) 只在 r > 0 或 PROPAGATE 状态下继续唤醒后继,按剩余许可传播,不是全量 unparkhasQueuedPredecessors() 只保证入队顺序,不解决惊群;高争用下限流器通常选非公平(允许抢占许可)nextFreeTicketMicros、滑动窗口),这已经超出 AQS 单变量能力按「底层原理 → 设计权衡 → 工程实践」递进排列。
考察方向:
tryAcquireShared 返回值语义与传播机制延伸递进面试题:
hasQueuedPredecessors 读队列 + 严格 FIFO 唤醒,高争用下 CAS 成功率低、上下文切换多nextFreeTicketMicros 字段 + 每次 acquire 时按 elapsed 补充许可,本质是令牌桶(Guava RateLimiter 的 SmoothBursty)jstack 中大量 WAITING→RUNNABLE 抖动、context switch 指标;改用按需唤醒 + 有界队列 + 快速失败【一句话结论】
面试官您好,如果只是"同时最多 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 令牌桶,别自己造。