Java 线程池(java.util.concurrent.ThreadPoolExecutor)的四种内置拒绝策略(Rejected Execution Handler)。当线程池无法接受新任务时(即工作队列已满且线程数达到 maximumPoolSize),就会触发拒绝策略。
目录
ThreadPoolExecutor.AbortPolicy (默认策略)
ThreadPoolExecutor.CallerRunsPolicy
ThreadPoolExecutor.DiscardPolicy
ThreadPoolExecutor.DiscardOldestPolicy
核心概念回顾:
-
核心线程数 (
corePoolSize): 即使空闲也会保留在池中的线程数(除非设置allowCoreThreadTimeOut)。 -
最大线程数 (
maximumPoolSize): 池中允许存在的最大线程数。 -
工作队列 (
workQueue): 用于存放等待执行任务的阻塞队列(如ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue)。 -
拒绝策略 (
RejectedExecutionHandler): 当任务提交被拒绝时(队列满 & 线程数达最大)执行的处理逻辑。
触发拒绝策略的条件:
-
线程池已被显式关闭 (
shutdown()或shutdownNow())。 -
线程池未关闭,但:
-
当前运行的线程数已达到
maximumPoolSize。 -
并且 工作队列已满(如果使用有界队列)。
-
四种内置拒绝策略:
-
ThreadPoolExecutor.AbortPolicy(默认策略)-
定义与行为: 直接抛出
RejectedExecutionException运行时异常,拒绝执行新任务。 -
解释: 这是一种“快速失败”(Fail-Fast) 策略。它明确告知调用者当前系统负载过高,无法处理新任务。调用者需要捕获这个异常并决定如何处理(重试、记录、降级等)。
-
适用业务场景:
-
需要严格保证任务不丢失的场景(且调用者有能力处理失败):如关键交易处理、订单创建、支付回调等,任务失败必须被感知并记录/告警/重试。
-
系统资源紧张,需要快速暴露问题:防止任务无限制堆积导致资源耗尽(OOM)。
-
高并发且任务重要性高:开发者希望明确知道任务何时被拒绝,以便采取相应措施。
-
-
示例代码:
import java.util.concurrent.*; public class AbortPolicyDemo { public static void main(String[] args) { // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=1, 拒绝策略=AbortPolicy ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, // corePoolSize 1, // maximumPoolSize 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), // 容量为1的有界队列 new ThreadPoolExecutor.AbortPolicy()); // 默认策略,可显式指定 // 提交3个任务 (超过队列+最大线程容量:1个运行中 + 1个在队列中 = 2,第3个触发拒绝) for (int i = 0; i < 3; i++) { final int taskId = i; try { executor.execute(() -> { System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName()); try { Thread.sleep(1000); // 模拟任务执行 } catch (InterruptedException e) { e.printStackTrace(); } }); } catch (RejectedExecutionException e) { System.err.println("Task " + taskId + " was rejected: " + e.getMessage()); // 在这里处理拒绝:记录日志、发送告警、尝试其他方案、重试(需谨慎)等 } } executor.shutdown(); // 记得关闭线程池 } }输出示例:
Task 0 executed by pool-1-thread-1 Task 1 executed by pool-1-thread-1 // 队列中的任务在第一个任务完成后执行 Task 2 was rejected: Task java.util.concurrent.FutureTask@... rejected from java.util.concurrent.ThreadPoolExecutor@...
-
-
ThreadPoolExecutor.CallerRunsPolicy-
定义与行为: 被拒绝的任务不会进入线程池,而是由提交该任务的调用者线程(通常是主线程或发起调用的线程) 直接执行。
-
解释: 这是一种“回退”(Fallback) 策略。它不会真正丢弃任务,而是让提交任务的线程自己去执行它。这有效地降低了新任务提交的速度,因为调用者线程忙于执行被拒绝的任务,暂时无法提交新任务,给线程池一个喘息的机会去处理队列中的任务。避免了任务丢失,但可能影响调用者线程的响应性。
-
适用业务场景:
-
任务不能丢失,且可以接受一定程度延迟或调用者线程被临时占用:如后台日志处理、非实时的数据同步、文件上传处理等。
-
需要一种温和的方式来控制任务提交速率:防止生产者速度过快压垮消费者(线程池),起到简单的负反馈作用。
-
Web服务器请求处理(需谨慎评估):如果调用者是Tomcat的HTTP工作线程,让它直接执行任务可能会阻塞该线程处理新HTTP请求的能力,但在某些低并发或特定任务下可用。
-
-
示例代码:
import java.util.concurrent.*; public class CallerRunsPolicyDemo { public static void main(String[] args) { // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=1, 拒绝策略=CallerRunsPolicy ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy()); // 指定CallerRunsPolicy System.out.println("Main thread: " + Thread.currentThread().getName()); // 提交3个任务 for (int i = 0; i < 3; i++) { final int taskId = i; executor.execute(() -> { System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); } executor.shutdown(); } }输出示例:
Main thread: main Task 0 executed by pool-1-thread-1 // 线程池线程执行 Task 1 executed by pool-1-thread-1 // 队列中的任务被线程池线程执行 Task 2 executed by main // 第三个任务被拒绝,由主线程(main)自己执行!
-
-
ThreadPoolExecutor.DiscardPolicy-
定义与行为: 默默地、直接丢弃 被拒绝的新任务,不做任何通知,也不抛出异常。
-
解释: 这是一种“静默丢弃”(Silent Drop) 策略。任务被丢弃如同从未提交过一样。调用者完全不知道任务被丢弃了。风险在于重要任务可能无声无息地丢失。
-
适用业务场景:
-
允许任务丢失且无需通知的场景:如不重要的监控采样数据、实时性要求不高且可容忍丢失的统计信息(如页面点击计数,丢几个点没关系)、频繁但非关键的心跳检测。
-
任务具有“可丢弃”属性:例如,如果同一个用户短时间内提交了多个相同的更新状态请求,丢弃掉一些旧的或中间的请求可能可以接受。
-
需要避免因拒绝任务导致上层逻辑中断(如避免抛出异常),且业务逻辑本身对任务丢失有容忍度。
-
-
示例代码:
import java.util.concurrent.*; public class DiscardPolicyDemo { public static void main(String[] args) { // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=1, 拒绝策略=DiscardPolicy ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy()); // 指定DiscardPolicy // 提交3个任务 for (int i = 0; i < 3; i++) { final int taskId = i; executor.execute(() -> { System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println("Task " + taskId + " submitted."); } executor.shutdown(); } }输出示例:
Task 0 submitted. Task 0 executed by pool-1-thread-1 Task 1 submitted. // Task1 进入队列 Task 2 submitted. // Task2 被提交时触发拒绝,被静默丢弃 Task 1 executed by pool-1-thread-1 // 队列中的Task1被执行 // 注意:Task2 没有任何输出,它被无声地丢弃了
-
-
ThreadPoolExecutor.DiscardOldestPolicy-
定义与行为: 当新任务被拒绝时,它会丢弃工作队列中等待最久的(队列头部的)那个任务,然后尝试重新提交当前这个新任务到队列中。
-
解释: 这是一种“弃旧纳新” 策略。它尝试牺牲队列中最早的任务来换取执行最新任务的机会。如果重新提交失败(可能队列在丢弃旧任务后瞬间又被新任务填满),则该新任务也会被丢弃。 有丢失任务的风险,且丢弃的是相对较旧的任务。
-
适用业务场景:
-
最新任务比旧任务更重要的场景:如实时状态更新(旧的坐标位置被新的覆盖)、新闻推送(最新的消息更有价值)、缓存刷新(旧缓存数据不如新数据重要)。
-
队列中的任务存在时效性,旧任务可能已失效或价值降低:如过期的价格查询请求、基于时间窗口的统计计算(旧数据可能不需要了)。
-
希望优先处理最新的请求,并愿意承担丢弃部分旧请求的风险。
-
-
示例代码:
import java.util.concurrent.*; public class DiscardOldestPolicyDemo { public static void main(String[] args) { // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=2, 拒绝策略=DiscardOldestPolicy ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(2), // 容量为2的队列 new ThreadPoolExecutor.DiscardOldestPolicy()); // 指定DiscardOldestPolicy // 提交4个任务 (超过容量:1运行 + 2队列 = 3,第4个触发拒绝) for (int i = 0; i < 4; i++) { final int taskId = i; executor.execute(() -> { System.out.println("Task " + taskId + " STARTED by " + Thread.currentThread().getName()); try { Thread.sleep(2000); // 延长任务执行时间,方便观察队列变化 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " FINISHED"); }); System.out.println("Task " + taskId + " submitted."); } try { Thread.sleep(1000); // 稍微等待一下,让任务有机会进入队列 } catch (InterruptedException e) { e.printStackTrace(); } // (可选) 此时可以检查队列,队列里应该是[Task1, Task2],Task0在运行。 // 提交Task3时触发拒绝:丢弃队列头的Task1,尝试将Task3加入队列尾。 executor.shutdown(); } }输出示例:
Task 0 submitted. Task 0 STARTED by pool-1-thread-1 // 核心线程执行Task0 Task 1 submitted. // Task1进入队列 Task 2 submitted. // Task2进入队列 Task 3 submitted. // 提交Task3时触发拒绝策略 // 策略动作:丢弃队列头部任务(Task1),尝试重新提交Task3 Task 0 FINISHED Task 3 STARTED by pool-1-thread-1 // 线程池线程取出并执行队列中新的队头任务Task3 (Task2还在队列里) Task 3 FINISHED Task 2 STARTED by pool-1-thread-1 // 线程池线程执行队列中最后的Task2 Task 2 FINISHED // 注意:Task1 被丢弃了,它既没有 STARTED 也没有 FINISHED 输出
-
总结与选择建议:
| 策略 (RejectedExecutionHandler) | 行为 | 是否丢失任务 | 是否通知调用者 | 特点/风险 | 适用场景 |
|---|---|---|---|---|---|
AbortPolicy (默认) | 抛出 RejectedExecutionException | 是 | 是 (异常) | 快速失败,强制调用者处理失败。可能导致业务中断。 | 任务关键,不允许静默丢失,需要明确感知失败。 |
CallerRunsPolicy | 调用者线程自己执行 | 否 | 是 (感知执行变慢) | 不丢弃任务,但降低提交速度。可能阻塞调用者线程。 | 任务重要可延迟,需控制提交速率,避免任务丢失。 |
DiscardPolicy | 默默丢弃新任务 | 是 | 否 | 完全静默。重要任务丢失无感知。 | 允许静默丢失的非关键任务(如采样、统计、日志)。 |
DiscardOldestPolicy | 丢弃队列头任务,重试提交新任务 | 是 (丢弃旧任务) | 否 | 弃旧纳新。可能丢失有价值的旧任务。 | 最新任务比旧任务更重要,旧任务可能已过时。(实时数据处理系统) |
选择原则:
-
任务重要性: 绝对不能丢?用
CallerRunsPolicy或自定义策略(如持久化)。可以丢?考虑Discard*或Abort。 -
业务容忍度: 能接受失败告警?用
AbortPolicy。能接受延迟?用CallerRunsPolicy。能接受静默丢失?用DiscardPolicy。新任务优先?用DiscardOldestPolicy。 -
系统目标: 需要快速暴露问题?选
AbortPolicy。需要系统持续运行即使牺牲部分任务?选Discard*或CallerRunsPolicy。 -
调用者影响: 调用者线程能否被占用执行任务?能则
CallerRunsPolicy可行。不能则避免。 -
队列性质: 如果队列是无界的(不推荐),理论上不会触发拒绝策略(除非关闭)。有界队列的大小直接影响触发拒绝的难易程度。
重要提示:
-
默认策略是
AbortPolicy。 -
可以通过
ThreadPoolExecutor的构造函数或setRejectedExecutionHandler方法设置拒绝策略。 -
如果内置策略都不满足需求,可以实现
RejectedExecutionHandler接口定义自己的拒绝逻辑(如将任务记录到日志、存入数据库稍后重试、转发到另一个队列等)。 -
合理配置
corePoolSize,maximumPoolSize和workQueue的大小是避免频繁触发拒绝策略的根本。需要根据具体业务负载和机器资源进行调优。 -
务必记得在不再需要线程池时调用
shutdown()或shutdownNow()来优雅关闭,释放资源。
&spm=1001.2101.3001.5002&articleId=148561736&d=1&t=3&u=b063a573c83e443d9360818f316aa285)
1757

被折叠的 条评论
为什么被折叠?



