面试官:线程池工作队列满了有哪些拒接策略?
今天我们来聊聊线程池中的一个重要话题——线程池工作队列满了之后,如何通过拒绝策略来处理任务。这是每个 Java 开发工程师都应该掌握的知识点,尤其是在高并发的场景下,如何优雅地应对线程池资源紧张的情况,直接关系到程序的稳定性和性能。
首先,我们要明确,线程池的作用是通过复用现有的线程来执行任务,避免频繁的创建和销毁线程。
线程池通常会维护一个任务队列,当有任务提交时,线程池会尝试将任务放入队列,等待空闲线程执行。但一旦队列满了,并且线程池的线程数也已达到最大值,那么就会发生任务积压,这时候就需要采用拒绝策略来处理这些无法执行的任务。
线程池在任务队列满了时,究竟该如何拒绝任务呢?常用的拒绝策略有四种,今天我们就逐一分析它们。
1. CallerRunsPolicy(调用者运行策略)
这是线程池中的一种比较“温和”的拒绝策略。当线程池的任务队列满了并且线程池的工作线程也无法继续执行任务时,CallerRunsPolicy会把任务交给提交任务的线程来执行。这意味着任务不会被丢弃或抛出异常,而是由调用者所在的线程执行。
这种策略有个优点,就是可以避免任务被丢弃或直接抛弃异常。缺点是,如果调用者线程的执行速度较慢,可能会导致系统的吞吐量下降,特别是在任务量非常大的情况下。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.CallerRunsPolicy()
);
上面的代码中,我们设置了一个线程池,最大线程数为 10,队列容量为 10。当队列满了时,CallerRunsPolicy会使提交任务的线程去执行任务,而不是丢弃或抛出异常。
2. AbortPolicy(中止策略)
AbortPolicy是默认的拒绝策略。如果任务队列满了并且线程池没有足够的线程来执行任务,AbortPolicy会直接抛出RejectedExecutionException异常,告诉调用者任务提交失败。这种策略是比较直接的,适合那些对任务提交要求比较严格的场景。举个例子,如果你希望在任务队列已满的情况下立即响应错误,而不是尝试重新提交任务或者丢弃任务,AbortPolicy就很适用。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.AbortPolicy()
);
使用这个策略时,如果任务提交到满载的线程池,程序会抛出异常,提示任务无法被处理。
3. DiscardPolicy(丢弃策略)
DiscardPolicy是一种非常简洁的拒绝策略。使用这个策略时,如果线程池的队列满了并且没有空闲线程,线程池会默默地丢弃提交的任务。丢弃的任务不做任何记录,不抛出异常,也不返回结果。可以理解为“任务丢了就丢了,谁也不说话”。
这种策略适合那些对丢弃任务没有太大影响的场景,比如日志记录或者监控数据的收集等。但是,如果丢弃的任务是非常重要的,可能会导致严重的后果,所以要谨慎使用。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.DiscardPolicy()
);
如果你在开发过程中遇到系统日志等不重要任务的处理,可以考虑使用这种策略,确保系统不会因为任务过多而崩溃。
4. DiscardOldestPolicy(丢弃最老任务策略)
DiscardOldestPolicy和DiscardPolicy类似,只不过它不是完全丢弃新提交的任务,而是选择丢弃队列中最旧的任务,尝试为新任务腾出位置。这个策略比较适用于某些任务队列中“新任务更重要”的场景。比如在一个实时系统中,最旧的任务可能已经不再重要,丢弃它可以为新的任务腾出空间。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
这种策略的优点是,系统能够尽量避免丢弃最新提交的任务,保证新任务能及时执行。缺点是最老的任务可能会被丢弃,如果最老的任务是很重要的,这个策略就不太适用了。
5. 自定义拒绝策略
除了这四种预设的拒绝策略,我们还可以根据业务需求自定义拒绝策略。通过实现RejectedExecutionHandler接口,我们可以定义一种更加灵活的任务拒绝方式。例如,可能希望在任务队列满时,将任务暂时缓存到其他地方,或者记录日志并延迟任务执行等。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义拒绝策略,例如记录日志
System.out.println("Task rejected: " + r.toString());
}
}
);
这种方式的灵活性非常高,你可以根据业务需求自由定义如何处理被拒绝的任务。
那面试官问:“线程池工作队列满了之后会怎么样?你能介绍一下常见的拒绝策略吗?”
你可以参考以下回答:
线程池在处理任务时,通常会先把任务提交到队列中,等待线程池中的线程去执行。如果队列满了且没有足够的空闲线程,线程池会根据配置的拒绝策略来决定如何处理任务。常见的拒绝策略有四种:
CallerRunsPolicy:让调用者线程来执行被拒绝的任务。这种策略的优点是不会丢弃任务,但可能会导致调用者线程变得非常忙碌。 AbortPolicy:直接抛出 RejectedExecutionException异常,通知调用者任务无法提交。这种策略适合那些希望尽早响应错误的场景。DiscardPolicy:直接丢弃被拒绝的任务,不做任何处理。适用于一些不重要的任务,如日志记录等。 DiscardOldestPolicy:丢弃任务队列中最老的任务,然后提交新的任务。适合那些“新任务更重要”的场景。
如果这些预设策略无法满足需求,我们还可以通过自定义RejectedExecutionHandler接口来实现自己的拒绝策略。
这就是线程池的拒绝策略的基本介绍!希望你对线程池的工作原理和拒绝策略有了更清晰的认识,遇到任务队列满的情况,能够灵活选择合适的策略来保证系统的稳定性。
-END-
ok,今天先说到这,老规矩,看完文章记得右下角给何老师点赞。
最后送给大家一个福利,我这里有一份搞副业的教程,这份教程里有100+个搞钱小项目:
网盘拉新核心玩法、公众号运营变现、小红书虚拟资料引流等,现在扫码加我微信,即可领取这份副业教程。
添加时备注:副业