面试题:线程池中的RejectedExecutionHandler策略
问题描述:
假设你正在设计一个应用,其中使用了ExecutorService
线程池来管理任务执行。然而,当线程池和队列都达到饱和状态时,新的任务提交将触发RejectedExecutionHandler
。请解释四种默认的RejectedExecutionHandler
策略,并实现一种自定义的RejectedExecutionHandler
策略,该策略应该在拒绝任务时记录详细的日志信息,并优雅地关闭最老的任务以尝试再次接纳新任务。
解答:
四种默认的RejectedExecutionHandler策略
-
AbortPolicy:抛出
RejectedExecutionException
异常,这是默认的策略。当线程池无法接受新任务时,此策略会简单地抛出异常,告知调用者无法执行任务。 -
CallerRunsPolicy:调用者的线程会执行任务。如果线程池已满,那么提交新任务的线程将执行该任务,而不是将其放入队列或等待。这可能导致提交任务的线程阻塞,直到任务完成。
-
DiscardPolicy:默默丢弃任务而不执行,不抛出异常,也不通知调用者。这种策略适用于可以容忍丢失任务的情况。
-
DiscardOldestPolicy:丢弃队列中最旧的任务,然后重新尝试执行任务(如果重试仍然失败,则重复此过程)。这意味着最近提交的任务将优先执行,而最旧的任务将被牺牲。
自定义RejectedExecutionHandler实现
下面展示如何实现一个自定义的RejectedExecutionHandler
,该策略在拒绝任务时记录日志,并尝试关闭最老的任务以接纳新任务。
import java.util.concurrent.*;
import java.util.logging.Logger;
public class CustomRejectHandler implements RejectedExecutionHandler {
private static final Logger LOGGER = Logger.getLogger(CustomRejectHandler.class.getName());
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录被拒绝任务的日志
LOGGER.warning("Task " + r.toString() + " rejected due to full thread pool and queue.");
if (!executor.isShutdown()) {
// 尝试移除队列中最老的任务
Runnable oldestTask = null;
if (!executor.getQueue().isEmpty()) {
oldestTask = executor.getQueue().poll();
if (oldestTask != null) {
LOGGER.info("Oldest task " + oldestTask.toString() + " removed from queue.");
}
}
// 再次尝试执行新任务
try {
executor.execute(r);
} catch (RejectedExecutionException ree) {
// 如果再次失败,记录日志并抛出异常
LOGGER.severe("Failed to execute task after removing the oldest one.");
throw ree;
}
}
}
}
在这个自定义的策略中,我们首先记录了被拒绝任务的日志信息。接着,如果线程池没有被显式关闭,我们将尝试从任务队列中移除最老的任务,然后再次尝试执行被拒绝的任务。如果再次尝试仍失败,我们将记录更严重的日志信息并抛出异常。
总结:通过这个例子,我们可以看到如何扩展Java并发框架的功能,以适应更复杂的应用场景。自定义RejectedExecutionHandler
允许我们根据应用的具体需求来处理线程池饱和时的任务拒绝情况,从而增强应用的健壮性和用户体验。