线程池队列满了会怎样?
面试里问"线程池队列满了怎么办",别急着背四个拒绝策略。
拒绝不是队列一满就发生。对一个手动创建的 ThreadPoolExecutor,它会先尝试补到核心线程数;核心线程忙完后,才优先把任务放进队列;队列塞不下,才继续创建非核心线程;线程已经到最大值,才交给拒绝处理器。

这几个参数放在一起看,顺序就很直观:
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
4,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy()
);
假设前几个任务都还没跑完。
- 第 1、2 个任务:直接创建核心线程执行。
- 第 3、4 个任务:核心线程已满,进入容量为 2 的队列。
- 第 5、6 个任务:队列满了,继续创建线程,最多扩到 4 个。
- 第 7 个任务:线程数和队列都到上限,才会触发
AbortPolicy,抛出RejectedExecutionException。

很多人答到这里会说"把队列调大就行"。这通常只是把超载藏起来。
无界队列会让 maximumPoolSize 基本失去作用:核心线程都忙时,后续任务一直排队,不会因为积压而扩到最大线程数。积压足够久,问题会从"被明确拒绝"变成"接口一直没返回,内存还在涨"。
有界队列不是为了制造拒绝,而是把延迟预算写进配置。比如单个任务平均 100 毫秒,4 个工作线程在稳定状态下每秒最多处理约 40 个任务。队列容量 200 意味着只看排队就可能多等约 5 秒。这个等待是否能接受,要由调用方的超时和业务时效决定。
四种默认拒绝策略,别按名字选
AbortPolicy 是默认值。它会抛异常,适合不能悄悄丢的任务。调用点要把它转换成可观测的过载信号,例如错误日志、指标和明确的失败响应。
CallerRunsPolicy 让提交任务的线程自己执行任务。它确实能给上游施加背压,但不要把它随手放进所有 Web 请求链路。调用线程如果是事件循环、持有锁,或者本身不能被长时间占用,背压会变成新的卡顿。
DiscardPolicy 会静默丢任务。除非业务天然允许丢弃,而且有丢弃计数和补偿手段,否则它最危险。日志里什么都没有,任务却已经没了。
DiscardOldestPolicy 会丢掉队列头部最早的任务,再重试提交当前任务。只在"新状态一定比旧状态更有价值"的场景才有讨论空间,例如可合并的最新状态刷新。对订单、通知、异步写库这类 FIFO 语义敏感的任务,不该用它赌运气。
生产里我更常把"拒绝"当成容量告警,而不是自动重试的起点。重试还是投回同一个已经饱和的池子,只会让队列更难退下来。
参数该从什么开始定?
先别从"CPU 核数乘几"起步,先把任务分开。
计算密集型任务,线程太多会把时间花在线程切换上;阻塞型任务可以容纳更多并发,但它占住的往往是数据库连接、下游 HTTP 配额或文件句柄。线程池最大值必须同时受这些下游资源约束。
然后拿高峰提交速率、单任务耗时、可接受排队时长推一个初始容量,再压测。观察至少三组数:活跃线程数、队列长度、拒绝次数。只有队列长度,没有拒绝次数,往往看不出"已经开始掉任务"还是"只是慢了一点"。
JDK 的 ThreadPoolExecutor 文档把这件事说得很明确:有限的最大线程数和有限队列同时耗尽时,任务才会被拒绝。面试里把这条路径讲清,再结合你的业务说为什么选有界队列和哪一种拒绝策略,已经比背参数表强很多。