线程池拒绝策略详解
当线程池无法接受新提交的任务时,就会触发拒绝策略(RejectedExecutionHandler)。拒绝策略是线程池的最后一道防线,用于应对系统过载或关闭状态下的任务处理。
1. 什么时候会触发拒绝?
在 ThreadPoolExecutor 中,以下情况会调用拒绝策略:
| 场景 | 说明 |
|---|---|
| 线程池已关闭 | 调用了 shutdown() 或 shutdownNow() 后,不再接受新任务。 |
| 队列已满且线程数已达上限 | 工作队列已满(有界队列),且当前工作线程数已经达到 maximumPoolSize,无法创建新线程。 |
| 队列已满且线程池已关闭 | 双重检查时发现线程池不再是 RUNNING 状态。 |
具体触发位置在 execute() 方法的最后一步:如果 addWorker(command, false) 失败(因为线程数已达上限),则执行 reject(command)。
2. 内置的四种拒绝策略
ThreadPoolExecutor 提供了四种内置策略,均实现了 RejectedExecutionHandler 接口。
| 策略类 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy(默认) |
抛出 RejectedExecutionException(非受检异常),任务被丢弃。 |
要求严格处理、不允许丢失任务的场景,由上层代码捕获异常并做补偿。 |
CallerRunsPolicy |
由提交任务的线程(调用者)自己执行该任务。如果线程池已关闭,则任务被丢弃。 | 希望减缓任务提交速度、降低系统压力,且不允许丢弃任务的场景。 |
DiscardPolicy |
静默丢弃任务,不抛异常,不通知。 | 允许部分任务丢失、对实时性要求不高(如日志记录、监控上报)。 |
DiscardOldestPolicy |
丢弃队列头部(最旧的未处理任务),然后重新提交当前任务(如果线程池未关闭)。 | 类似 DiscardPolicy,但优先淘汰旧任务,保证新任务有机会执行。 |
2.1 AbortPolicy(默认)
java
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " + e.toString());
}
}
- 特点:快速失败,明确告知调用方任务被拒绝。
- 风险:调用方如果不捕获异常,任务会丢失且可能中断业务流程。
- 实践:在关键业务中,调用方应捕获异常,进行重试、降级或持久化。
2.2 CallerRunsPolicy
java
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run(); // 直接在当前线程(调用者线程)中运行
}
}
}
- 特点:将任务"回退"给调用者线程执行,这会降低任务提交速率(因为调用者要花时间执行任务)。
- 优点:不会丢弃任何任务,同时利用调用者的执行时间作为天然的限流机制。
- 缺点:如果调用者是一个快速返回的线程(如 Tomcat 工作线程),执行耗时任务可能导致其阻塞,影响其他请求。
- 实践:适合后台批处理、非实时任务,或者不希望任务丢失但能接受短暂阻塞的场景。
2.3 DiscardPolicy
java
public static class DiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 什么都不做,静默丢弃
}
}
- 特点:静默丢弃,不抛异常,不记录日志。
- 风险:任务完全丢失,且无任何感知,排查问题困难。
- 实践:极少直接使用。一般用于允许少量丢失的辅助任务(如记录访问日志、发送非关键通知)。如果使用,建议在自定义拒绝策略中至少记录一条警告日志。
2.4 DiscardOldestPolicy
java
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll(); // 丢弃队列头部的任务
e.execute(r); // 重新提交当前任务
}
}
}
- 特点:丢弃最旧的等待任务,给新任务让位。
- 适用:新任务比旧任务更有价值(如实时消息、最新请求)。
- 风险:旧任务可能很重要(如数据库写操作),丢弃会造成数据不一致。
- 实践:需要评估任务优先级,通常配合有界队列使用,并记录丢弃日志。
3. 拒绝策略的执行时机与注意事项
3.1 线程池关闭时的特殊行为
当线程池处于 SHUTDOWN 状态时:
- 不再接受新任务,但会继续处理队列中的任务。
- 如果此时提交新任务,所有拒绝策略(包括
CallerRunsPolicy)都会检查isShutdown(),若已关闭则直接丢弃,不会执行r.run()。
因此,CallerRunsPolicy 并不能保证任务一定被执行,线程池关闭后提交的任务同样会丢失。
3.2 拒绝策略中的重入风险
DiscardOldestPolicy 内部调用了 e.execute(r),这可能会再次触发拒绝(如果线程池仍然处于饱和状态)。虽然代码中已经做了 if (!e.isShutdown()) 判断,但理论上仍可能形成循环。不过实际实现中,execute 再次调用拒绝策略时,可能会再次进入 DiscardOldestPolicy,导致递归或重复丢弃。但一般不会出现严重问题,因为队列中旧任务被丢弃后,新任务有机会进入队列。
3.3 自定义拒绝策略中的资源释放
如果任务持有重要资源(如数据库连接、文件句柄),在拒绝时应该显式释放,否则可能造成资源泄漏。例如:
java
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (r instanceof ResourceHolder) {
((ResourceHolder) r).cleanup();
}
throw new RejectedExecutionException("Task rejected");
}
4. 自定义拒绝策略
通过实现 RejectedExecutionHandler 接口,可以扩展更多行为,例如:
- 记录日志并抛出异常
- 将任务写入持久化存储(数据库、消息队列)以便后续重试
- 将任务放入另一个"备份"线程池
- 根据任务优先级动态决定丢弃或降级
- 发送告警通知
示例:带日志和监控的自定义拒绝策略
java
public class LoggingRejectedHandler implements RejectedExecutionHandler {
private static final Logger logger = LoggerFactory.getLogger(LoggingRejectedHandler.class);
private final AtomicLong rejectCount = new AtomicLong(0);
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
long count = rejectCount.incrementAndGet();
logger.warn("Task {} rejected from {}. Total rejects: {}",
r, executor.toString(), count);
// 可选:发送告警
if (count % 100 == 0) {
sendAlert("ThreadPool rejection count reached " + count);
}
// 默认仍然抛出异常(可选)
throw new RejectedExecutionException("Task rejected");
}
}
示例:降级到备份线程池
java
public class FallbackRejectedHandler implements RejectedExecutionHandler {
private final ExecutorService fallbackExecutor;
public FallbackRejectedHandler(ExecutorService fallbackExecutor) {
this.fallbackExecutor = fallbackExecutor;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
fallbackExecutor.submit(r);
}
}
}
示例:阻塞式拒绝策略(慎用)
有些场景希望任务提交方阻塞等待,直到线程池有空闲。虽然不推荐(容易造成死锁),但可以实现:
java
public class BlockingRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
try {
// 阻塞直到队列有空间(只对 BlockingQueue 有效)
executor.getQueue().put(r);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RejectedExecutionException("Interrupted", e);
}
}
}
}
⚠️ 注意:getQueue().put(r) 会永久阻塞,且可能破坏线程池内部状态(队列本应通过 offer 而非 put 操作)。强烈不推荐在生产环境使用。
5. 如何设置拒绝策略
5.1 构造时指定
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, 20, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() // 指定拒绝策略
);
5.2 运行时修改
java
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
5.3 使用 Spring 线程池配置
java
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
6. 场景选择指南
| 业务场景 | 推荐拒绝策略 | 理由 |
|---|---|---|
| 核心交易链路(如支付、下单) | AbortPolicy + 上层捕获重试/降级 |
必须明确感知失败,不能静默丢弃;调用方可做补偿(如放入消息队列)。 |
| 非核心异步任务(如发送通知、日志) | DiscardPolicy 或 DiscardOldestPolicy |
允许少量丢失,对业务无重大影响。 |
| 流量控制/限流 | CallerRunsPolicy |
利用调用者线程执行任务,减缓提交速度,保护系统不被压垮。 |
| 批量处理任务(如数据导入) | CallerRunsPolicy 或自定义持久化 |
不希望丢失任务,且可以接受处理速度降低。 |
| 实时性要求高的场景(如秒杀) | AbortPolicy + 快速返回失败 |
宁可拒绝新请求,也不让旧任务排队导致延迟飙升。 |
| 与消息队列配合 | 自定义:将任务转发到 MQ | 彻底解耦,保证任务不丢失,但增加系统复杂度。 |
7. 常见问题与陷阱
7.1 CallerRunsPolicy 导致主线程阻塞
如果提交任务的线程是 Tomcat 的请求处理线程,而拒绝策略让该线程执行一个耗时任务(如复杂计算、长时 IO),会阻塞其他请求,造成请求超时或线程池耗尽。
解决:为不同优先级任务使用不同线程池;或者确保被回退的任务本身足够轻量。
7.2 静默丢弃任务导致数据不一致
使用 DiscardPolicy 或 DiscardOldestPolicy 时,如果没有日志记录,出了问题很难排查。建议在自定义拒绝策略中至少记录 WARN 级别日志。
7.3 DiscardOldestPolicy 丢弃重要任务
旧任务可能是数据库批量更新操作,丢弃后会导致数据丢失。使用前要评估任务优先级,或者只在非关键路径使用。
7.4 线程池关闭后提交任务
即使设置了 CallerRunsPolicy,线程池关闭后任务仍会被丢弃(因为 isShutdown() 为 true)。如果需要在关闭后还能执行任务,应该自定义策略忽略关闭状态(但通常不推荐,因为线程池关闭意味着应用要停止)。
7.5 拒绝策略中的异常传播
AbortPolicy 抛出的是运行时异常,如果调用方不捕获,会导致当前线程终止。对于 Web 应用,可能导致请求返回 500 错误。需要根据业务决定是否全局捕获。
8. 完整示例:生产级自定义拒绝策略
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicLong;
public class ProductionRejectedHandler implements RejectedExecutionHandler {
private static final Logger logger = LoggerFactory.getLogger(ProductionRejectedHandler.class);
private final AtomicLong rejectedCount = new AtomicLong(0);
private final String poolName;
public ProductionRejectedHandler(String poolName) {
this.poolName = poolName;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
long count = rejectedCount.incrementAndGet();
String message = String.format("[%s] Task rejected. Pool: size=%d, active=%d, queue=%d, completed=%d, rejectCount=%d",
poolName,
executor.getPoolSize(),
executor.getActiveCount(),
executor.getQueue().size(),
executor.getCompletedTaskCount(),
count);
logger.warn(message);
// 可选:发送告警(如接入 Prometheus AlertManager)
if (count % 1000 == 0) {
sendAlert(message);
}
// 根据任务类型决定是抛出异常还是降级
if (isCriticalTask(r)) {
throw new RejectedExecutionException("Critical task rejected: " + r);
} else {
// 非关键任务静默丢弃或记录到死信队列
logger.debug("Non-critical task discarded: {}", r);
}
}
private boolean isCriticalTask(Runnable r) {
// 通过任务名称、类型等判断
return r.getClass().getSimpleName().startsWith("Critical");
}
private void sendAlert(String message) {
// 调用告警接口,如发送邮件、钉钉、Slack
}
}
9. 总结
| 策略 | 行为 | 适用场景 | 风险 |
|---|---|---|---|
AbortPolicy |
抛异常 | 核心业务,需明确失败 | 调用方不处理则任务丢失 |
CallerRunsPolicy |
调用者执行 | 限流、保护系统 | 调用者线程可能被阻塞 |
DiscardPolicy |
静默丢弃 | 非关键任务 | 无感知丢失 |
DiscardOldestPolicy |
丢弃最旧,重试当前 | 新任务优先 | 旧任务丢失 |
| 自定义 | 灵活扩展 | 特殊需求(持久化、备份、告警) | 实现复杂度 |
最佳实践:
- 生产环境避免使用默认的
AbortPolicy而不做任何处理,至少要在上层捕获异常或记录日志。 - 总是使用有界队列,配合明确的拒绝策略,防止 OOM 和无限等待。
- 监控拒绝次数,设置阈值告警,及时发现容量不足。
- 对于关键业务,考虑在拒绝策略中实现降级逻辑(如写入本地文件或消息队列,稍后重试)。
理解并正确选择拒绝策略,是构建高可用、弹性系统的关键一步。