⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 小哈学Java 「犬小哈」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

面试考察点

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

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

核心答案

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

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

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

深度解析

原理与机制

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

代码示例与使用方式

import java.util.concurrent.*;

publicclass 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++) {
finalint 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. 配置与监控:合理设置 corePoolSize、maximumPoolSize 和 workQueue 容量是根本。同时,应通过 JMX 或监控系统对线程池的活跃线程数、队列大小等指标进行监控。

常见误区

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

总结

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

文章目录
  1. 1. 面试考察点
  2. 2. 核心答案
  3. 3. 深度解析
    1. 3.1. 原理与机制
    2. 3.2. 代码示例与使用方式
    3. 3.3. 对比分析与最佳实践
    4. 3.4. 常见误区
  4. 4. 总结