跳到主要内容

队列、拒绝策略与过载

线程池无法消灭过载。任务到达速度长期高于处理速度时,只能让调用方等待、让队列增长、拒绝部分工作或增加真正可用的下游容量;队列与拒绝策略决定压力在哪一层暴露。

1. 队列容量决定延迟和内存上限

  • ArrayBlockingQueue 容量固定,内存和最大排队量容易估算。
  • LinkedBlockingQueue 可以设置容量;省略容量时上限很大,不代表业务上安全。
  • SynchronousQueue 不保存任务,每次提交必须直接交给可用 Worker,常用于快速扩容模型。
  • 优先级或延迟队列会改变顺序,还要考虑低优先级任务饥饿。

队列容量可以从允许的排队时间反推。服务每秒稳定处理 500 个任务,若最多接受 200 ms 排队,初始估算约为 100 个任务,再用任务大小和突发模型验证。

2. 四种内置拒绝策略表达不同语义

策略行为主要风险
AbortPolicy抛出 RejectedExecutionException调用方必须显式处理
CallerRunsPolicy由提交线程执行延迟传播、事件循环被阻塞、重入
DiscardPolicy静默丢弃任务丢失不可见
DiscardOldestPolicy丢弃队首,再尝试提交旧任务可能更重要,顺序被破坏

默认优先考虑显式失败,再由业务决定重试、降级或返回繁忙。只有任务允许丢失且有指标时,才使用丢弃方案。

3. CallerRuns 是有限的反压手段

提交线程自己运行任务,会降低它继续提交的速度,因此在同步生产者链路中可以形成反馈。但它有明确边界:

  • HTTP I/O 线程可能被长任务占住。
  • 单线程事件循环一旦执行阻塞任务,会拖住其他连接。
  • 持锁提交时,任务可能在锁内意外执行。
  • 上游还有缓存队列时,压力只会转移。

使用前要追踪“谁是调用者”,不能只记住“CallerRuns 不丢任务”。

4. 过载策略要覆盖业务结果

拒绝后至少需要明确:

  • 调用方看到哪个错误码或异常。
  • 是否允许重试,退避和幂等如何保证。
  • 哪些任务可以合并、采样或只保留最新值。
  • 队列深度、最老任务年龄和拒绝次数怎样告警。

无界队列把拒绝推迟为超时或 OOM,通常使失败更晚、更难恢复。

5. 隔离比总池扩容更重要

快任务和慢任务共用线程池时,慢下游可以占满所有 Worker。按依赖、优先级或延迟目标拆分执行器,相当于建立舱壁;每个池都要有自己的容量预算和监控。

拆池不会增加数据库连接数等真实容量。总并发仍应受下游上限约束,避免多个池合计把依赖压垮。

6. 常见问题

6.1 队列越大越能抗突发吗

更大的队列能吸收更长突发,也会增加长尾、内存和过期任务数量。超过请求期限的任务即使最终执行也可能没有价值。

6.2 拒绝是不是线程池配置失败

不一定。容量有界的系统在过载时必须拒绝或降级;可预期、可观测的拒绝通常比无限积压更健康。

7. 面试题

7.1 线程池队列满了以后应该怎样处理

出现公司:美团、阿里巴巴

考察重点

  • 最大线程与拒绝触发条件。
  • 四种策略的业务后果。
  • 有界队列、反压和可观测性。

相关内容:第 1 节“队列容量决定延迟和内存上限”至第 4 节“过载策略要覆盖业务结果”。

参考回答

队列满后,线程数未到 maximum 时线程池会尝试增加 Worker;仍无法接收才调用拒绝策略。生产系统应优先让失败显式可见,再按业务选择返回繁忙、有限重试、降级、合并或允许丢弃。

CallerRuns 能在部分同步链路降低提交速度,但可能阻塞 I/O 或事件循环线程。无论哪种策略,都要监控队列深度、最老任务年龄和拒绝数,并保证任务幂等与截止时间。