线程池决绝策略

6 阅读9分钟

线程池拒绝策略详解

当线程池无法接受新提交的任务时,就会触发拒绝策略(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(默认)

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

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();   // 直接在当前线程(调用者线程)中运行
        }
    }
}
  • 特点:将任务“回退”给调用者线程执行,这会降低任务提交速率(因为调用者要花时间执行任务)。
  • 优点:不会丢弃任何任务,同时利用调用者的执行时间作为天然的限流机制。
  • 缺点:如果调用者是一个快速返回的线程(如 Tomcat 工作线程),执行耗时任务可能导致其阻塞,影响其他请求。
  • 实践:适合后台批处理、非实时任务,或者不希望任务丢失但能接受短暂阻塞的场景。

2.3 DiscardPolicy

public static class DiscardPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        // 什么都不做,静默丢弃
    }
}
  • 特点:静默丢弃,不抛异常,不记录日志。
  • 风险:任务完全丢失,且无任何感知,排查问题困难。
  • 实践:极少直接使用。一般用于允许少量丢失的辅助任务(如记录访问日志、发送非关键通知)。如果使用,建议在自定义拒绝策略中至少记录一条警告日志。

2.4 DiscardOldestPolicy

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 自定义拒绝策略中的资源释放

如果任务持有重要资源(如数据库连接、文件句柄),在拒绝时应该显式释放,否则可能造成资源泄漏。例如:

public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
    if (r instanceof ResourceHolder) {
        ((ResourceHolder) r).cleanup();
    }
    throw new RejectedExecutionException("Task rejected");
}

4. 自定义拒绝策略

通过实现 RejectedExecutionHandler 接口,可以扩展更多行为,例如:

  • 记录日志并抛出异常
  • 将任务写入持久化存储(数据库、消息队列)以便后续重试
  • 将任务放入另一个“备份”线程池
  • 根据任务优先级动态决定丢弃或降级
  • 发送告警通知

示例:带日志和监控的自定义拒绝策略

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");
    }
}

示例:降级到备份线程池

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);
        }
    }
}

示例:阻塞式拒绝策略(慎用)

有些场景希望任务提交方阻塞等待,直到线程池有空闲。虽然不推荐(容易造成死锁),但可以实现:

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 构造时指定

ThreadPoolExecutor pool = new ThreadPoolExecutor(
    5, 20, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy()  // 指定拒绝策略
);

5.2 运行时修改

pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

5.3 使用 Spring 线程池配置

@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 + 上层捕获重试/降级必须明确感知失败,不能静默丢弃;调用方可做补偿(如放入消息队列)。
非核心异步任务(如发送通知、日志)DiscardPolicyDiscardOldestPolicy允许少量丢失,对业务无重大影响。
流量控制/限流CallerRunsPolicy利用调用者线程执行任务,减缓提交速度,保护系统不被压垮。
批量处理任务(如数据导入)CallerRunsPolicy 或自定义持久化不希望丢失任务,且可以接受处理速度降低。
实时性要求高的场景(如秒杀)AbortPolicy + 快速返回失败宁可拒绝新请求,也不让旧任务排队导致延迟飙升。
与消息队列配合自定义:将任务转发到 MQ彻底解耦,保证任务不丢失,但增加系统复杂度。

7. 常见问题与陷阱

7.1 CallerRunsPolicy 导致主线程阻塞

如果提交任务的线程是 Tomcat 的请求处理线程,而拒绝策略让该线程执行一个耗时任务(如复杂计算、长时 IO),会阻塞其他请求,造成请求超时或线程池耗尽。

解决:为不同优先级任务使用不同线程池;或者确保被回退的任务本身足够轻量。

7.2 静默丢弃任务导致数据不一致

使用 DiscardPolicyDiscardOldestPolicy 时,如果没有日志记录,出了问题很难排查。建议在自定义拒绝策略中至少记录 WARN 级别日志。

7.3 DiscardOldestPolicy 丢弃重要任务

旧任务可能是数据库批量更新操作,丢弃后会导致数据丢失。使用前要评估任务优先级,或者只在非关键路径使用。

7.4 线程池关闭后提交任务

即使设置了 CallerRunsPolicy,线程池关闭后任务仍会被丢弃(因为 isShutdown() 为 true)。如果需要在关闭后还能执行任务,应该自定义策略忽略关闭状态(但通常不推荐,因为线程池关闭意味着应用要停止)。

7.5 拒绝策略中的异常传播

AbortPolicy 抛出的是运行时异常,如果调用方不捕获,会导致当前线程终止。对于 Web 应用,可能导致请求返回 500 错误。需要根据业务决定是否全局捕获。


8. 完整示例:生产级自定义拒绝策略

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 和无限等待。
  • 监控拒绝次数,设置阈值告警,及时发现容量不足。
  • 对于关键业务,考虑在拒绝策略中实现降级逻辑(如写入本地文件或消息队列,稍后重试)。

理解并正确选择拒绝策略,是构建高可用、弹性系统的关键一步。