线程池决绝策略

线程池拒绝策略详解

当线程池无法接受新提交的任务时,就会触发拒绝策略(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 + 上层捕获重试/降级 必须明确感知失败,不能静默丢弃;调用方可做补偿(如放入消息队列)。
非核心异步任务(如发送通知、日志) 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. 完整示例:生产级自定义拒绝策略

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

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

相关推荐
Moe4882 小时前
WebSocket :从浏览器 API 到 Spring 握手、Handler 与前端客户端
java·后端·架构
神奇小汤圆2 小时前
探索springboot程序打包docker的最佳方式
后端
邦爷的AI架构笔记2 小时前
我用Claude API接入了CI/CD安全扫描,踩了这几个坑
后端
henujolly3 小时前
go学习第一天
后端
毕业设计-小慧3 小时前
计算机毕业设计springboot城市休闲垂钓园管理系统 基于Spring Boot的都市休闲垂钓基地数字化运营平台 城市智慧钓场综合服务管理平台
spring boot·后端·课程设计
Nyarlathotep01133 小时前
ReentrantReadWriteLock基础和原理
java·后端
woniu_maggie4 小时前
SAP CPI 开发RFC适配器的Integration Flow
后端
树獭叔叔4 小时前
Agent 记忆系统设计全景:从短期对话到长期知识沉淀
后端·aigc·openai