你可能常常听到"线程池",这就像是你家厨房里的大锅炖菜,任务一波接一波地过来,锅里不断地添新材料。每当锅满了,厨房就会爆炸,对吧?没错,任务拒绝策略 就是这锅溢出的时候,厨房老板会如何优雅地解决------让过多的菜退出厨房,或者让一些菜转到外卖送到其他地方去。
但问题来了,厨房老板不可能每次都让菜溢出到地上 。有时候,他会做出非常聪明的处理措施,保持厨房的高效运转。这不就是我们在写代码时遇到的 线程池的任务拒绝策略 吗?当线程池满了,队列排不下新任务,或者任务的负载太高时,任务拒绝策略 会决定任务的命运。
是不是有点心急?放松点,我们一起来深入探讨这个看似简单却内里复杂的机制。
1. 什么是任务拒绝策略?
任务拒绝策略就像是线程池的"应急预案",当线程池中的工作线程忙不过来,任务队列也填满了,它就会通过这个机制处理无法继续接受的任务。拒绝策略的关键在于保证线程池的稳定性,避免系统因为过多的任务积压而崩溃。
Java 中的 ThreadPoolExecutor 提供了几种常见的拒绝策略,默认情况下是 AbortPolicy ,但我们可以通过自定义策略来应对不同的场景。
2. AbortPolicy ------ 默默拒绝,直接抛异常
这是最直接也最"果断"的拒绝策略。任务如果超过了线程池的最大承载能力,它会直接抛出 RejectedExecutionException,告诉你:"任务提交失败,赶紧回去处理。"

java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.AbortPolicy());
适用场景:
-
当任务失败时必须立刻得到反馈。
-
业务系统需要强烈的错误警告,不能容忍任务丢失。
但是,AbortPolicy 也有它的缺点。如果你没做好错误处理,任务丢失的那一刻,你的系统就像断了腿一样痛苦------根本跑不动。除非是特别急需失败反馈的系统,否则一般我们不推荐默认使用它。
3. CallerRunsPolicy ------ 任务抛给提交者,自己解决
这就是传说中的 "自己动手,丰衣足食" 。当任务提交到线程池时,CallerRunsPolicy 会将任务回退到提交任务的线程中去执行。简而言之,它让提交任务的线程自己来处理任务,自己当厨师,自己做菜。
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.CallerRunsPolicy());
适用场景:
-
任务提交者希望承担某些超出线程池任务量限制的任务。
-
可以接受线程池无法及时处理任务时的延迟执行。
这个策略的好处是,它避免了任务的丢失,也不会让系统崩溃,但是有个明显的问题:如果提交任务的线程负担过重,反而会引发 "雪崩效应",使得系统无法承受,性能急剧下降。简言之,虽然任务不会丢失,但可能会造成系统负载过高。
4. DiscardPolicy ------ 最低要求的"脏活"策略
如果你不想让系统崩溃,不希望回退给提交线程,而又不在乎丢弃掉那些无法执行的任务,DiscardPolicy 会是你的选择。它简单粗暴,直接 丢掉 最早的任务------这就像是外卖服务拒绝了某个订单,没告诉你具体原因,只是说: "你去找别家吧"。
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.DiscardPolicy());
适用场景:
-
不需要对丢失的任务做任何反应。
-
一些不太重要、可丢弃的任务,比如日志处理、统计任务等。
劣势 :这就意味着你 完全不关心任务丢失了。虽然很爽,但有时候丢弃的任务可能正是关键任务。你想好了吗?
5. DiscardOldestPolicy ------ 先丢掉排队队列里最老的任务
这也是一种丢弃策略,但它和 DiscardPolicy 不一样,它丢弃的不是提交的任务,而是队列里 最老的任务 。它就像一个餐馆老板发现排队的顾客太多,直接让排在队尾的最老顾客先走,给新来的顾客腾出位置。
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.DiscardOldestPolicy());
适用场景:
-
任务对顺序要求不严格,可以接受先到的任务被丢弃。
-
适用于那些任务的执行时间差异较大,能够忍受某些任务丢失的场景。
虽然看起来很实用,但是 DiscardOldestPolicy 有一个隐患:如果任务堆积越来越多,丢弃最老的任务会加剧资源的浪费,导致不可预见的后果。所以,如果你选择这个策略,确保任务的优先级和处理顺序不会造成不可接受的影响。
6. 拒绝策略的自定义:适配复杂场景
当然了,线程池的默认拒绝策略未必能完全满足你的需求。你可以 自定义拒绝策略 ,根据具体的业务场景设计任务丢弃或者回退的方式。比如,某些任务如果失败,就需要进行 重试 ,这时候可以实现一个 重试机制,将任务重试提交到另一个备用的线程池中。

java
public class RetryPolicy implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义重试机制
System.out.println("任务被拒绝,进行重试...");
executor.submit(r);
}
}
通过这种方式,你可以实现更灵活、更智能的任务拒绝策略。看似简单的拒绝策略,背后却有着非常强大的扩展能力,帮助你应对更加复杂的业务需求。
总结:拒绝策略------线程池的安全阀
线程池的拒绝策略,其实就是线程池的"安全阀"。它决定了在高并发、任务堆积的情况下,如何优雅地将问题化解,保证系统在最坏情况下也不会崩溃。每种拒绝策略都有它的适用场景,选择合适的拒绝策略不仅能让系统稳定运行,还能提高资源利用效率。
不过,拒绝策略虽然是最后一道防线,但绝不能依赖它作为系统的常规运作模式。拒绝任务意味着资源的浪费和任务的丢失,因此,合理配置线程池大小、优化任务调度、监控线程池的健康状态,才是提高系统可靠性和性能的根本途径。
下一次,你的面试官再问你线程池的拒绝策略时,你可得从容地回答:"我懂,拒绝策略背后可是有讲究的!"