threadpoolexecutor拒绝策略仅在workqueue满且线程数达maximumpoolsize后提交任务时触发,需结合队列类型、execute()调用、提交线程上下文及监控指标综合判断。

ThreadPoolExecutor 拒绝策略触发的典型场景
拒绝策略只在任务提交时触发,不是线程池满就立刻执行,而是当 workQueue 也满了、且线程数已达 maximumPoolSize 后,再提交新任务才会走拒绝逻辑。常见误判是看到 CPU 或线程忙就以为触发了拒绝,其实可能只是任务在队列里排队——得确认 getQueue().size() 和 getActiveCount() 才能定位。
- 队列类型影响极大:
LinkedBlockingQueue默认无界,几乎不触发拒绝;ArrayBlockingQueue有界才可控 - 提交用
execute()才会触发拒绝策略;submit()底层也调用execute(),同样生效 - 拒绝发生在主线程(提交者线程),不是工作线程,所以策略里的操作不能太重,否则拖慢提交方
四种内置拒绝策略的行为差异
AbortPolicy(默认):直接抛 RejectedExecutionException,适合强实时系统,但需调用方显式捕获处理。
CallerRunsPolicy:由提交任务的线程自己执行该任务。能缓解压力,但可能让业务线程卡住,尤其在高并发 HTTP 请求场景下,导致请求响应变慢甚至超时。
DiscardPolicy:静默丢弃,不报错也不执行。适合日志采集等允许丢失的场景,但难以监控是否真丢了任务。
DiscardOldestPolicy:丢掉队列头的任务,再尝试重新提交当前任务。前提是队列支持 poll()(如 ArrayBlockingQueue),对 PriorityBlockingQueue 可能丢错优先级任务。
自定义拒绝策略要注意线程安全和可观测性
自定义策略本质是实现 RejectedExecutionHandler 接口,但容易忽略两点:
- 策略方法
rejectedExecution(Runnable r, ThreadPoolExecutor executor)在提交线程中运行,若在里面做日志聚合、发告警、写磁盘等操作,会阻塞所有后续提交 - 缺少计数器或埋点,导致过载发生时无法区分是突发流量还是配置不合理。建议搭配
AtomicLong记录丢弃次数,并通过 Micrometer 或 Prometheus 暴露指标
public class LoggingRejectHandler implements RejectedExecutionHandler {
private final AtomicLong rejectedCount = new AtomicLong();
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
long count = rejectedCount.incrementAndGet();
if (count % 100 == 0) { // 降频打点
System.err.println("Task rejected, total: " + count);
}
// 不做耗时操作:不 sleep、不 IO、不锁大对象
}
}
配置拒绝策略前必须检查队列容量与核心参数匹配
很多人设了 DiscardPolicy 却发现任务照常执行——其实是队列没满。关键检查点:
- 确认构造时传的是有界队列:
new ArrayBlockingQueue(100),而非new LinkedBlockingQueue() -
corePoolSize和maximumPoolSize的关系影响扩容时机;若两者相等,队列满后立刻触发拒绝,没有“新建线程”缓冲 - 使用
allowCoreThreadTimeOut(true)时,即使corePoolSize > 0,空闲核心线程也会被回收,实际并发能力可能低于预期
真正难的不是选哪个策略,而是把「队列水位」「活跃线程数」「拒绝计数」三者关联起来看趋势。单独调一个参数,往往掩盖了真实瓶颈。










