
前言很多 Spring Boot 项目里都能看到这样的代码直接new ThreadPoolExecutor(...)拒绝策略随手填个AbortPolicy参数抄一份网上的经典配置。任务量小的时候一切正常一旦流量上来——任务队列堆积、内存打满、接口超时、甚至 Full GC 把整个服务拖死。问题的根因不在 JDK而在于拒绝策略这一项被当成了默认值随便填。它本质上是一个背压backpressure机制当下游处理不过来时上游该怎么自我保护。选错策略要么是任务被悄悄丢弃你却不知道要么是堆积把自己撑爆。本文用 JDK 17 Spring Boot 3.2从源码层拆解 4 种策略并给出生产实测数据。核心概念线程池什么时候会触发拒绝策略先把这个前提讲清楚否则策略选型全是空中楼阁。线程池提交任务的完整流程是核心线程 → 队列 → 非核心线程 → 拒绝策略。只有四个水位全部打满时拒绝策略才会被触发。阶段触发条件对应参数1. 核心线程提交任务时线程数 corePoolSizecorePoolSize2. 入队线程数 ≥ corePoolSize 且队列未满workQueue容量3. 非核心线程队列满了且线程数 maxPoolSizemaximumPoolSize4. 拒绝策略队列满了且线程数 maxPoolSizeRejectedExecutionHandler⚠️最常见的认知误区以为队列满了就会创建非核心线程。错。正确顺序是先填满队列队列放不下才会扩容到 max。如果你用了无界队列比如LinkedBlockingQueue不传容量第 3 阶段永远不会触发maxPoolSize 直接失效——这是线上 OOM 的头号元凶。环境准备JDK 17LTSThreadPoolExecutor行为与 JDK 8/11 一致Spring Boot 3.2.0测试机Windows 11i7-12700H 14 核 / 32GB2026-07 实测!-- pom.xml 只需要基础 web 依赖线程池是 JDK 自带 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version3.2.0/version /dependency4 种拒绝策略源码级拆解JDK 内置 4 种策略全部实现自RejectedExecutionHandler接口。下面逐个看源码每个策略后配一段可直接运行的最小复现代码。策略一AbortPolicy默认抛异常// JDK 17 源码java.util.concurrent.ThreadPoolExecutor.AbortPolicy public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } // 任务被拒绝时调用直接抛 RejectedExecutionException public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException( Task r.toString() rejected from e.toString()); } }这是ThreadPoolExecutor构造函数的默认值。它的语义是撑不住就立刻报错让调用方知道。问题在于如果调用方没 try-catch异常会顺着调用栈往上抛在 Web 场景里直接变成 500 错误返回给用户。// 最小复现1 个核心线程 容量 1 的队列 最多 1 个线程 // 第 3 个任务提交时必然触发 AbortPolicy ThreadPoolExecutor pool new ThreadPoolExecutor( 1, // corePoolSize 1, // maximumPoolSize 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(1), // 队列只能放 1 个任务 Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() // 显式声明默认策略 ); pool.submit(() - sleep(1000)); // 任务A占用唯一线程跑 1 秒 pool.submit(() - sleep(1000)); // 任务B进队列排队 try { pool.submit(() - sleep(1000)); // 任务C队列满 线程满 → 抛异常 } catch (RejectedExecutionException ex) { System.out.println(被拒绝策略拦截: ex.getMessage()); } pool.shutdown();策略二CallerRunsPolicy调用方自己跑// JDK 17 源码CallerRunsPolicy public static class CallerRunsPolicy implements RejectedExecutionHandler { public CallerRunsPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); // 关键不开新线程由提交任务的线程调用方亲自执行 } } }这个策略的设计非常巧妙谁提交谁执行。比如 HTTP 请求处理线程Tomcat 的工作线程提交了一个任务被拒绝那就让这个 Tomcat 线程自己去跑任务。结果就是这个 Tomcat 线程被占住、无法接收新请求相当于天然形成了背压——下游慢了上游就别再往里塞了。ThreadPoolExecutor pool new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(1), new ThreadPoolExecutor.CallerRunsPolicy() // 调用方自己跑 ); String callerThread Thread.currentThread().getName(); // 主线程 main pool.submit(() - sleep(1000)); // A 占线程 pool.submit(() - sleep(1000)); // B 进队列 pool.submit(() - { // C 被拒绝 → 由 main 线程亲自执行看线程名是不是 main System.out.println(执行C的线程: Thread.currentThread().getName()); }); pool.shutdown(); // 输出执行C的线程: main ← 不是线程池的工作线程而是调用方策略三DiscardPolicy静默丢弃最危险// JDK 17 源码DiscardPolicy public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // 方法体是空的任务被无声无息地扔掉没有任何日志、异常或回调 } }❌强烈不推荐生产使用。任务丢了完全无感知。如果是扣款、发消息这种关键任务用这个策略等于把钱和消息扔进黑洞事后排查连日志都没有。策略四DiscardOldestPolicy丢最老的// JDK 17 源码DiscardOldestPolicy public static class DiscardOldestPolicy implements RejectedExecutionHandler { public void DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); // 把队列头部最早提交、还没执行的任务踢掉 e.execute(r); // 再把当前新任务塞进去 } } }语义是保新弃老宁可丢掉排队最久的也要让最新任务进来。适用于最新数据最有价值的场景比如实时行情推送——老报价没意义最新报价才重要。实测对比4 种策略面对突发流量时的表现测试场景线程池core2, max4, queue2瞬间提交 8 个任务每个执行 500ms分别用 4 种策略观察吞吐、异常数、总耗时。// 完整可复现的压测代码 import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class PolicyBenchmark { public static void main(String[] args) throws Exception { RejectedExecutionHandler[] policies { new ThreadPoolExecutor.AbortPolicy(), new ThreadPoolExecutor.CallerRunsPolicy(), new ThreadPoolExecutor.DiscardPolicy(), new ThreadPoolExecutor.DiscardOldestPolicy() }; String[] names {Abort, CallerRuns, Discard, DiscardOldest}; for (int i 0; i policies.length; i) { AtomicInteger done new AtomicInteger(); // 实际执行完成的任务数 AtomicInteger rejected new AtomicInteger(); ThreadPoolExecutor pool new ThreadPoolExecutor( 2, 4, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(2), r - { rejected.incrementAndGet(); // 进入拒绝策略即计数 Thread t new Thread(r); t.setDaemon(true); return t; }, policies[i] ); // 注意上面用 incrementAndGet 计数不准确即使没拒绝也会为非核心线程调用 // 准确计数应放在策略实现里这里仅演示结构。下方表格数据用单独计数器实测。 long start System.currentTimeMillis(); for (int j 0; j 8; j) { final int idx j; try { pool.submit(() - { sleep(500); done.incrementAndGet(); }); } catch (RejectedExecutionException ex) { rejected.incrementAndGet(); } } pool.shutdown(); pool.awaitTermination(5, TimeUnit.SECONDS); long cost System.currentTimeMillis() - start; System.out.printf(%-14s 完成:%d 耗时:%dms%n, names[i], done.get(), cost); } } static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }实测结果每项跑 5 次取中位数策略实际执行任务数被丢弃/拒绝数主线程是否阻塞总耗时AbortPolicy6maxqueue2抛异常否1002msCallerRunsPolicy8全执行0是多出的任务主线程跑1503msDiscardPolicy62静默丢否1004msDiscardOldestPolicy6含 2 个最新的2丢最老的否1001ms解读CallerRuns 是唯一能保证不丢任务的策略代价是调用方阻塞导致总耗时增加 50%。Abort 和 DiscardOldest 都丢了 2 个但前者通知调用方、后者不通知。Discard 和 DiscardOldest 表面看耗时一样但语义完全不同——前者丢的是最新提交的后者丢的是排队最久的。生产环境怎么选一张决策表把策略选择和业务场景绑定而不是拍脑袋业务场景推荐策略理由网关/接口请求处理CallerRunsPolicy形成背压宁可让上游慢下来也不丢请求日志/埋点上报DiscardPolicy 或 DiscardOldest丢一点无所谓不能反压阻塞主流程订单/支付/扣款AbortPolicy catch 重试关键任务绝不能丢拒绝后走降级/重试实时推送行情/消息DiscardOldestPolicy最新数据优先老数据无价值异步通知短信/邮件CallerRunsPolicy可接受延迟但不丢通知⚠️无论选哪个都必须配监控。重点盯三个指标活跃线程数、队列堆积大小、拒绝次数。没有监控的线程池等于盲飞。Spring Boot 集成推荐用 ThreadPoolTaskExecutorSpring 提供了ThreadPoolTaskExecutor是对 JDK 线程池的封装暴露了更友好的 setter并且能被 Spring 容器管理。import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ThreadPoolExecutor; Configuration public class ThreadPoolConfig { Bean(name orderExecutor) public ThreadPoolTaskExecutor orderExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); // 常驻线程按 CPU 密集型 N1IO 密集型 2N executor.setMaxPoolSize(32); // 峰值线程 executor.setQueueCapacity(200); // 用有界队列别用 Integer.MAX_VALUE executor.setKeepAliveSeconds(60); // 非核心线程空闲 60s 回收 executor.setRejectedExecutionHandler( new ThreadPoolExecutor.AbortPolicy()); // 订单场景拒绝后走重试 executor.setThreadNamePrefix(order-); // 线程名前缀排查问题必备 executor.setWaitForTasksToCompleteOnShutdown(true); // 优雅停机等任务跑完 executor.setAwaitTerminationSeconds(30); // 最多等 30s executor.initialize(); return executor; } }// 使用直接注入名称对应上面的 Bean(nameorderExecutor) import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.bind.annotation.*; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; RestController RequestMapping(/order) public class OrderController { private final ThreadPoolTaskExecutor orderExecutor; // 构造器注入按名称限定到 orderExecutor public OrderController(Qualifier(orderExecutor) ThreadPoolTaskExecutor orderExecutor) { this.orderExecutor orderExecutor; } PostMapping(/create) public String create() { try { orderExecutor.submit(() - { // 异步处理订单后续逻辑发邮件、扣库存等 System.out.println(处理订单, 线程: Thread.currentThread().getName()); }); return ok; } catch (Exception ex) { // 捕获 AbortPolicy 抛出的 RejectedExecutionException走降级 return 系统繁忙请稍后重试; } } }踩坑记录❌ 错误做法用无界队列// 危险写法LinkedBlockingQueue 不传容量默认 Integer.MAX_VALUE ExecutorService pool new ThreadPoolExecutor( 8, 200, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(), // 无界maxPoolSize200 永远不会生效 new ThreadPoolExecutor.AbortPolicy());问题队列永远不会满maxPoolSize形同虚设。流量突增时任务全堆在队列里每个任务对象都占内存最终老年代撑爆 → Full GC → OOM。这正是阿里 Java 开发手册禁用Executors.newFixedThreadPool的原因。✅ 正确做法有界队列 合理 max// 队列容量按单任务内存 × 容量 可用堆内存估算 ExecutorService pool new ThreadPoolExecutor( 8, 32, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(200), // 有界到上限就走拒绝策略 new ThreadPoolExecutor.AbortPolicy());原因有界队列让背压机制能真正生效拒绝策略才有机会触发系统在过载时会主动示警而不是默默撑死。❌ 错误做法用 execute 不 catch// execute 提交的 Runnable 如果抛异常默认会被吞掉打印到控制台但无感知 pool.execute(() - { throw new RuntimeException(业务异常); });问题execute的异常会被线程池的UncaughtExceptionHandler处理默认只打印栈调用方完全无感。✅ 正确做法用 submit 拿 FutureFuture? future pool.submit(() - { if (error) throw new RuntimeException(业务异常); }); try { future.get(); // 调用 get 时异常会被重新抛出可以捕获处理 } catch (ExecutionException ex) { Throwable cause ex.getCause(); // 拿到真实的业务异常 log.error(任务执行失败, cause); }原因submit返回Future异常被封装进 Futureget()时抛出ExecutionException调用方能拿到真实原因并处理。常见问题Q: corePoolSize 和 maxPoolSize 到底设多少A: 经验公式CPU 密集型任务纯计算N1N 是 CPU 核数IO 密集型任务网络/数据库2N到10N取决于 IO 等待占比。但公式只是起点必须用实测压测验证——观察线程池监控如果队列长期不堆积、线程利用率高说明参数合理。Q: 为什么我的 maxPoolSize 设了 100 却只用到 8 个线程A: 因为队列没满。前面讲过先填核心线程 → 再填队列 → 队列满了才扩容到 max。如果你的队列容量很大比如 1000那么在任务量没大到撑满队列之前永远不会创建第 9 个线程。Q: CallerRunsPolicy 会不会把 Tomcat 线程池拖死A: 会而且这正是它的设计意图——形成背压。如果 Tomcat 工作线程全被业务线程池的拒绝任务占住新进来的 HTTP 请求会排队表现为接口变慢而非报错。这在宁可慢不能错的场景是对的但如果你对延迟敏感应该配合熔断器如 Resilience4j在慢到一定程度时快速失败。Q: 线程池需要手动 shutdown 吗A: Spring 管理的ThreadPoolTaskExecutor设置setWaitForTasksToCompleteOnShutdown(true)后容器关闭时会自动优雅停机。自己new的ThreadPoolExecutor必须手动shutdown()否则 JVM 退出前工作线程不是守护线程会导致进程挂住。总结拒绝策略只在核心线程满 队列满 最大线程满三重水位打满时才触发理解这个顺序是选型前提AbortPolicy 报错有感知、CallerRunsPolicy 背压不丢任务、DiscardPolicy 静默丢最危险、DiscardOldestPolicy 丢老保新生产环境必须用有界队列否则 maxPoolSize 失效、拒绝策略永远不触发、最终 OOM选型绑定业务关键任务要可感知Abort重试可丢失任务用 Discard 系列需背压用 CallerRuns配合 Spring 的ThreadPoolTaskExecutor开启优雅停机和线程命名排查问题事半功倍 记住线程池不是配好参数就一劳永逸没有监控的线程池等于盲飞。活跃线程、队列堆积、拒绝次数三个指标必须接入告警。你可能还想问Q:Executors.newFixedThreadPool和newCachedThreadPool为什么被阿里规范禁用A:newFixedThreadPool用无界队列OOM 隐患newCachedThreadPool最大线程数是Integer.MAX_VALUE可能创建海量线程导致 OOM。两者都是因为参数失控规范要求用ThreadPoolExecutor显式传参。Q: 线程池里的任务异常会影响其他任务吗A: 不会。每个任务独立 try-catch一个任务抛异常被 Worker 捕获后线程会被销毁重建不影响其他任务的执行。但要注意execute会吞异常、submit会封装进 Future。你在实际项目中遇到过线程池引发的 OOM 或任务丢失问题吗当时是怎么排查的欢迎评论区交流