线程池的拒绝策略有哪些?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官询问线程池的拒绝策略,其核心考察点远不止于让你背诵四种策略的名称。他/她更希望了解:

  1. 对线程池运行原理的掌握:你是否理解任务被拒绝的前置条件(队列满且线程数达到最大值)?
  2. 对每种策略内涵的理解与应用场景判断:你是否清楚每种策略的具体行为,并能根据业务场景(如重要性、可丢弃性、实时性要求)做出合理选择?
  3. 实战经验与设计思想:你是否在实际项目中配置或自定义过拒绝策略?这反映了你的问题排查、资源管理和系统保护意识

核心答案

当线程池的任务队列已满,且工作线程数已达到最大线程数 maximumPoolSize 时,新提交的任务将会触发拒绝策略(RejectedExecutionHandler)

JDK 的 ThreadPoolExecutor 类内置了四种拒绝策略,均为其内部类:

  1. AbortPolicy(中止策略,默认策略):直接抛出 RejectedExecutionException 异常,由调用者捕获处理。
  2. CallerRunsPolicy(调用者运行策略):不抛弃任务,也不抛出异常,而是将任务回退给调用者(即提交任务的线程)执行。
  3. DiscardPolicy(丢弃策略):静默地丢弃新提交的任务,不抛异常,也无任何通知。
  4. DiscardOldestPolicy(丢弃最老策略):丢弃队列头部的(即下一个将要被执行的最老)任务,然后尝试重新提交当前新任务。

深度解析

原理与机制

拒绝策略是线程池的最后一道安全阀。其设计遵循了 “饱和对策” 这一通用系统设计原则,旨在处理过载情况。ThreadPoolExecutorexecute(Runnable command) 方法在无法入队且无法创建新线程时,会调用 rejectedExecution(command, this) 方法,这正是拒绝策略的逻辑入口。

代码示例与使用方式

import java.util.concurrent.*;

public class RejectionPolicyDemo {
    public static void main(String[] args) {
        // 创建一个核心线程为2,最大线程为4,队列容量为2的线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, // corePoolSize
            4, // maximumPoolSize
            60, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2) // 容量为2的有界队列
            // 可以通过最后一个参数显式指定拒绝策略,不指定则默认为 AbortPolicy
            //, new ThreadPoolExecutor.AbortPolicy()
        );

        // 模拟提交超过处理能力的任务(共提交7个任务,最多处理6个:4个线程+2个队列)
        for (int i = 1; i <= 7; i++) {
            final int taskId = i;
            try {
                executor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " 执行任务:" + taskId);
                    try {
                        Thread.sleep(1000); // 模拟任务耗时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            } catch (RejectedExecutionException e) {
                // 使用 AbortPolicy 时,第7个任务会触发此异常
                System.err.println("任务 " + taskId + " 被拒绝: " + e.getMessage());
            }
        }
        executor.shutdown();
    }
}

对比分析与最佳实践

策略行为优点缺点适用场景
AbortPolicy抛出异常明确失败反馈,便于上层业务进行降级、告警或重试。若调用方未处理异常,可能导致流程中断。通用型、关键业务。这是默认策略,因为它强制开发者关注并处理过载问题。
CallerRunsPolicy调用者执行实现一种简单的负反馈。提交任务的线程被占用执行任务,自然会降低新任务的提交速度,给线程池喘息之机。任务不会被丢失可能阻塞调用者(如Tomcat的HTTP处理线程),影响整体吞吐。不允许任务丢失、且任务的执行耗时可控的场景。如一些计算密集型任务。
DiscardPolicy静默丢弃简单,无额外开销。任务数据无声无息丢失,难以追踪和排查问题。不重要的、可容忍丢失的异步任务,如无关紧要的统计日志。需慎用
DiscardOldestPolicy丢弃队头尝试为最新的任务让出空间。会丢弃一个正在等待的、相对较老的任务,可能导致业务逻辑不完整。后提交的任务优先级更高,且可以容忍丢弃老任务的场景。如实时消息流,新消息比旧消息价值更高。

最佳实践

  1. 理解默认策略:默认使用 AbortPolicy 是 JDK 的良苦用心,提醒你线程池不是无底洞,需要合理配置和监控。
  2. 自定义策略是常见做法:对于复杂业务,自定义拒绝策略往往是最佳选择。例如,将拒绝的任务持久化到数据库或消息队列,待负载降低后重新投递;或记录详细日志并触发告警。
    public class CustomRejectionHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // 1. 记录日志(任务信息、线程池状态)
            // 2. 持久化任务到Redis/Kafka
            // 3. 触发监控告警
            System.err.println("任务被拒绝,已记录并告警。");
            // 注意:在此 handler 中执行耗时操作需谨慎,因为它运行在提交任务的线程中
        }
    }
    
  3. 配置与监控:合理设置 corePoolSizemaximumPoolSizeworkQueue 容量是根本。同时,应通过 JMX 或监控系统对线程池的活跃线程数、队列大小等指标进行监控。

常见误区

  • 误区一:认为只要用了线程池,任务就一定会被执行。事实:错误的配置和不匹配的拒绝策略会导致任务被丢弃或系统崩溃。
  • 误区二:盲目使用 DiscardPolicyDiscardOldestPolicy 以求 “系统稳定”。事实:这掩埋了问题,可能导致核心业务数据丢失,使得线上问题更难被发现和追溯。

总结

四种拒绝策略本质上是 “快速失败”、“延迟降级”、“静默丢弃”和“弃老保新” 四种不同过载处理哲学的实现;选择时,务必结合业务容忍度、任务重要性进行权衡,自定义策略通常是满足复杂业务需求的更优解。