队列、拒绝策略与过载
线程池无法消灭过载。任务到达速度长期高于处理速度时,只能让调用方等待、让队列增长、拒绝部分工作或增加真正可用的下游容量;队列与拒绝策略决定压力在哪一层暴露。
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 或事件循环线程。无论哪种策略,都要监控队列深度、最老任务年龄和拒绝数,并保证任务幂等与截止时间。