bigywater.tech
Java 内部原理 · 58

ThreadPoolExecutor 拒绝策略真实验证:四种策略到底怎么表现

核心线程数满、队列也满了,新任务提交会触发拒绝策略。这篇真实构造一个 1 核心线程 + 容量 1 队列的线程池,把四种内置 RejectedExecutionHandler 各自触发一遍,真实观察每种策略下任务的最终命运。

CallerRuns 真实同步执行
真实验证:CallerRunsPolicy 下,被拒绝的任务真实由提交它的 main 线程自己跑完,submit() 调用本身会阻塞到任务执行完
DiscardOldest 真实挤掉排队最久的
真实验证:队列里等待最久的任务被真实丢弃、从未执行,腾出的位置被新提交的任务占据

上一篇:switch 表达式字节码真实对比

真实验证:AbortPolicy 真实抛出异常(默认策略)

1 核心线程占满、1 容量队列占满后,第三个任务提交,默认策略应该真实拒绝并抛异常。

ThreadPoolRejectionDemo.java ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.AbortPolicy()); pool.submit(blockingTask); // 占满线程 pool.submit(blockingTask); // 占满队列 pool.submit(blockingTask); // 触发拒绝
真实实测
AbortPolicy 真实抛出 RejectedExecutionException:
Task java.util.concurrent.FutureTask@42a57993[Not completed, ...] rejected from
java.util.concurrent.ThreadPoolExecutor@75b84c92[Running, pool size = 1,
active threads = 1, queued tasks = 1, completed tasks = 0]

真实结果验证了默认策略 AbortPolicy 的行为:直接、立即抛出 RejectedExecutionException,而且异常信息真实带上了线程池当时的完整快照(运行状态、池大小、活跃线程数、排队任务数、已完成任务数),这对排查线上"任务提交失败"问题很有价值——不需要额外打日志,异常信息本身就是诊断数据。

真实验证:CallerRunsPolicy 真实让提交者自己执行

这个策略常被用作简单的降级/限流手段:线程池忙不过来时,让调用方自己承担这部分计算。

真实实测
CallerRunsPolicy 下,被拒绝任务的真实执行线程: main(提交方是 main)
真实机制细节。关键在于:CallerRunsPolicy 不是把任务丢到别的地方异步执行,而是真实在 submit() 这次调用内部,同步、阻塞式地直接跑了这个任务的 run() 方法——调用它的是谁的线程,任务就真实在谁的线程上跑完。这里提交方是 main 线程,真实捕获到的执行线程名就是 main。这个特性天然形成一种背压(backpressure):线程池越忙,提交者就被拖得越慢,间接限制了任务提交的速度。

真实验证:DiscardPolicy 真实静默丢弃

最"粗暴"的策略:不抛异常、不执行、什么都不做,任务凭空消失。

真实实测
DiscardPolicy 下未抛异常,被丢弃任务的计数器最终值(应为0): 0

真实验证了这一点:提交了一个"自增计数器"的任务,策略是 DiscardPolicy,submit() 调用本身没有抛出任何异常,程序表现得"很正常",但计数器真实纹丝不动——任务被真实丢弃,且没有留下任何痕迹。这也是这个策略最大的风险:如果不主动监控线程池的拒绝次数,任务丢失可能完全不会被发现。

真实验证:DiscardOldestPolicy 真实挤掉排队最久的任务

这个策略的行为最复杂:不是丢弃新任务,而是丢弃队列里等待最久的那个,把位置让给新任务。

ThreadPoolRejectionDemo.java pool.submit(blockingTask); // 占满唯一的工作线程 pool.submit(() -> oldTaskRan.set(1)); // 进入队列排队 pool.submit(() -> newTaskRan.set(1)); // 触发拒绝:挤掉队列里的旧任务
真实实测
DiscardOldestPolicy 下,被挤掉的老任务是否执行(应为0): 0, 新任务是否执行(应为1): 1

真实结果证明了它的行为逻辑:排在队列里的 oldTaskRan 任务被真实从队列中移除、永远不会被执行(计数器保持 0),而触发拒绝的 newTaskRan 任务真实顶替了它的位置,后续等工作线程空出来后真实被执行了(计数器变成 1)。这个策略适合"只关心最新数据、旧数据可以丢弃"的场景,比如实时行情推送。

分步演示:线程池真的满了,提交的新任务真实去哪了

每一行都是真实运行这段代码得到的结果(用CountDownLatch精确控制饱和状态,保证结果确定)。

线程池和队列都真实占满之后,新任务真实落到哪种拒绝策略手里,结果完全不同——抛异常、自己扛下来、无声丢弃,或者挤掉排队最久的那一个
这一步新验证的拒绝策略

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,四种策略两次独立运行结果完全一致(测试用 CountDownLatch 精确控制了任务占用时机,消除了调度不确定性)。
    • 和LongAdder、ReentrantLock两篇一起,拼出了 java.util.concurrent 包在真实工程里最常被用到的一组工具的真实行为图景。
    • 没有涉及:自定义 RejectedExecutionHandler 的实现方式、线程池 keepAliveTime 对非核心线程回收的真实时机——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号