必须传rejectedexecutionhandler,因为当线程池满(core/max均满且队列满)时需明确任务处理策略,否则抛rejectedexecutionexception;jdk四种策略不满足所有场景,自定义需实现接口并注意线程安全、非空判断、避免递归和阻塞。

为什么直接 new ThreadPoolExecutor 时必须传 RejectedExecutionHandler
因为线程池在 corePoolSize 和 maximumPoolSize 都满、且工作队列也满时,必须明确告诉 JVM “接下来这个新任务怎么处理”,否则会直接抛 RejectedExecutionException。JDK 提供的四种内置策略(如 AbortPolicy、CallerRunsPolicy)只是默认选项,不能满足所有业务场景——比如你希望记录日志后降级为异步重试,或转发到 Kafka 备份队列。
自定义 RejectedExecutionHandler 的正确写法
实现 RejectedExecutionHandler 接口,重写 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法即可。注意两点:该方法在调用线程(即提交任务的线程)中执行,不是在线程池内部线程里;它不抛异常,但你自己写的逻辑如果出错,可能静默吞掉问题。
- 不要在
rejectedExecution中阻塞太久(比如同步发 HTTP 请求),否则会拖慢上游调用方 - 避免在其中再次调用
executor.execute(r),极易引发无限递归或 StackOverflow - 若需重试,建议用独立线程池或消息队列解耦,而不是原地 retry
- 务必对
r做非空判断,某些场景下r可能为 null(虽少见,但 JDK 源码有注释说明)
示例:
public class LoggingRejectHandler implements RejectedExecutionHandler {
private static final Logger log = LoggerFactory.getLogger(LoggingRejectHandler.class);
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r != null) {
log.warn("Task rejected: {}, pool size: {}, queue size: {}",
r.getClass().getSimpleName(),
executor.getPoolSize(),
executor.getQueue().size());
}
// 这里可选:丢给备用线程池、写入本地文件、发告警等
}
}
常见踩坑:用 Lambda 写拒绝策略导致隐式引用泄漏
有人图省事这样写:
立即学习“Java免费学习笔记(深入)”;
new ThreadPoolExecutor(..., (r, e) -> {
log.warn("rejected: " + r);
});
问题在于:Lambda 若捕获了外部变量(比如某个 Spring Bean 或大对象),会导致该对象无法被 GC,尤其当线程池长期存活时,内存泄漏风险明显。更稳妥的做法是用静态内部类或独立类,确保无意外闭包。
- 不要在 Lambda 里直接引用
this、非 static 字段、或上下文对象 - 如果必须带上下文(如 traceId),应显式传参,而非靠闭包捕获
- IDE 通常会警告“lambda captures instance reference”,看到就该警惕
拒绝策略和队列类型组合的实际影响
拒绝策略是否触发,不仅取决于线程数,还和队列行为强相关。比如:
-
LinkedBlockingQueue默认无界(容量为Integer.MAX_VALUE),几乎不会触发拒绝,除非内存耗尽——这时RejectedExecutionHandler形同虚设 -
ArrayBlockingQueue是有界队列,配合CallerRunsPolicy可让提交线程自己执行任务,适合流量削峰但需注意调用方响应时间突增 -
SynchronousQueue不存储任务,每个execute()都必须立刻有空闲线程接手,否则立即触发拒绝——这是最“激进”的组合,适合高实时性场景
所以别只盯着拒绝策略本身,先确认你的队列是不是真能起到缓冲作用。
自定义拒绝策略真正难的不是写几行代码,而是想清楚:这个任务丢了会怎样?重试是否安全?谁来兜底?这些决策比语法细节重要得多。










