线程池是 Java 并发编程中绕不开的基础组件,但很多同学对它的理解停留在“提交任务、自动执行”的层面,很少关注队列塞满之后的兜底行为。当核心线程、最大线程、工作队列全部达到上限时,新提交的任务会触发拒绝策略(RejectedExecutionHandler)。JDK 内置了四种策略,其中 DiscardOldestPolicy 是一个比较特别的存在:它不是丢弃新任务,而是丢掉队列里排队最久的老任务,腾出位置让新任务进来。这种“以新换旧”的思路非常适合任务有时效性的场景,比如行情推送、实时监控指标上报,老任务过期后价值会大打折扣,与其继续排队不如直接丢弃。

一、拒绝策略在什么条件下被触发
要理解 DiscardOldestPolicy,先要弄清楚拒绝策略的触发时机。线程池执行任务的核心入口是 execute 方法,内部的判断顺序是:如果当前线程数小于核心线程数(corePoolSize),直接创建新线程执行;如果线程数已达核心数,任务被放入工作队列;如果队列也放不下了,且线程数小于最大线程数(maximumPoolSize),会继续创建非核心线程;只有当线程数达到最大值且队列仍然满的时候,才会调用拒绝处理器。
换句话说,拒绝策略是线程池的最后一道防线,它被触发意味着线程池的处理能力已经完全跟不上任务的产生速度。常见的触发原因包括:任务提交速率远超消费速率、核心线程数和队列容量设置得过小、任务执行耗时突然变长导致堆积,或者下游依赖变慢拖住了工作线程。生产环境出现这类问题时,往往表现为接口响应变慢、任务莫名丢失或直接抛出 RejectedExecutionException。
四种内置策略的行为差异可以用一张表来概括:
| 策略 | 行为 | 典型场景 |
|---|---|---|
| AbortPolicy | 抛出 RejectedExecutionException | 默认策略,任务不能丢的场景 |
| CallerRunsPolicy | 由提交任务的线程自己执行 | 允许提交方减速,起反压作用 |
| DiscardPolicy | 静默丢弃当前新任务 | 允许丢新数据,如日志上报 |
| DiscardOldestPolicy | 丢弃队列头部最老任务,重试提交新任务 | 数据有时效性,新数据优先 |
二、DiscardOldestPolicy 的源码逻辑与适用前提
这个策略的源码非常短,但每一行都有讲究。看一下 JDK 中的实现:
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
// 关键点一:只有线程池未关闭时才处理
e.getQueue().poll();
// 关键点二:丢掉队列头部等待最久的任务
e.execute(r);
// 关键点三:重试提交当前任务,仍可能再次触发拒绝
}
}
}第一行的 isShutdown 判断很重要。线程池已经进入关闭流程时,任何新任务都不应该被接收,所以这里直接静默返回,相当于丢弃。第二行的 getQueue().poll() 会从队列头部取出(也就是移除)等待时间最长的任务,注意这里只是取出来直接扔掉,没有任何回调或通知,任务从此彻底消失。第三行重新调用 execute,此时队列大概率有了一个空位,新任务可以被放入队列。
这里有一个容易被忽视的细节:重试提交并非百分之百成功。如果是在高并发下,多个线程同时触发拒绝策略,A 线程刚通过 poll 腾出的位置可能瞬间被 B 线程的新任务占走,A 再次调用 execute 仍可能失败,形成二次拒绝甚至无限循环的风险很低但确实存在。另外,如果队列用了 SynchronousQueue 这种不存储元素的队列,poll 操作永远是 null,策略相当于变相丢弃当前任务,和 DiscardPolicy 行为一致。因此 DiscardOldestPolicy 必须搭配有容量的队列(如 LinkedBlockingQueue 或 ArrayBlockingQueue)才有意义。
三、完整代码示例:观察任务的丢弃与执行
下面用一个可以直接运行的例子来验证策略效果。构造一个核心线程数为 1、最大线程数为 1、队列容量为 3 的线程池,让工作线程每次 sleep 一段时间模拟慢消费,然后一次性提交 6 个任务,观察哪些任务被执行、哪些被丢弃:
import java.util.concurrent.*;
public class DiscardOldestDemo {
public static void main(String[] args) throws InterruptedException {
// 单线程 + 容量为3的队列,最多只能容纳4个任务
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(3),
new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 1; i <= 6; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("任务 " + taskId + " 开始执行,线程: "
+ Thread.currentThread().getName());
try {
Thread.sleep(500); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("任务 " + taskId + " 执行完成");
});
System.out.println("任务 " + taskId + " 已提交");
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("线程池已关闭");
}
}运行这段代码,你会看到任务 1 被线程直接执行,任务 2、3、4 依次进入队列。当任务 5 提交时队列已满,触发 DiscardOldestPolicy,队列头部的任务 2 被 poll 出来丢弃,任务 5 入队。任务 6 提交时再次触发拒绝,这次被丢弃的是任务 3。最终结果是被执行的是任务 1、4、5、6,任务 2 和 3 静默消失,控制台不会有任何异常或日志。
正因为丢弃是完全静默的,生产环境使用时强烈建议做定制化改造。常见做法是自己实现 RejectedExecutionHandler,在丢弃前记录日志、打点监控或者把任务序列化到磁盘待后续补偿。例如下面的增强版策略,保留了 DiscardOldestPolicy 的行为,同时补上了可观测性:
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
public class LoggedDiscardOldestPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
Runnable oldest = e.getQueue().poll();
if (oldest != null) {
// 丢弃前记录日志,方便排查任务丢失问题
System.out.println("丢弃最老任务: " + oldest
+ ",当前队列剩余: " + e.getQueue().size());
}
e.execute(r);
}
}
}四、使用时的注意事项与方案选型
DiscardOldestPolicy 最大的风险点在于“无感知丢失”。任务被丢弃时不抛异常、不打日志,如果业务上不允许丢数据(比如订单处理、支付回调),用这个策略等于埋雷。它适合的前提是任务本身有时效属性:数据过期后执行没有意义,比如实时行情快照、传感器数据上报、页面访问统计这类场景,最新的数据永远比排队的旧数据更有价值。
如果任务确实重要又想控制队列长度,可以考虑几个替代或补充方案。第一,改用 CallerRunsPolicy,让提交线程亲自执行任务,天然形成反压,代价是提交方的吞吐会下降;第二,把队列换成可动态调整容量的结构,配合监控在高峰期扩容;第三,使用带超时的 offer 方案,入队失败后等待一小段时间再降级处理,给线程池一点喘息空间;第四,在任务对象里加时间戳字段,消费时判断是否过期自行跳过,把丢弃逻辑从拒绝策略下沉到业务层,控制粒度更细。
总结一下选型思路:默认的 AbortPolicy 适合大多数不能丢任务的场景,出问题快速失败反而容易排查;CallerRunsPolicy 适合可以接受提交方减速的批处理场景;DiscardPolicy 和 DiscardOldestPolicy 只在明确允许数据丢失、且数据新旧有优先级差异时使用。无论选哪种,都建议在代码里显式指定而不是依赖默认值,并配合线程池监控(活跃线程数、队列长度、已完成任务数)做容量评估,避免线上才发现队列早已打满。
ThreadPoolExecutorDiscardOldestPolicy线程池拒绝策略修改时间:2026-09-15 22:36:49