线程池拒绝策略最佳实践

2,770 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

线程池配置的几个参数之中,拒绝策略可能是大家最容易忽略的一个参数。在一般并发不是特别高或者任务数不是特别多的系统中,基本用不到拒绝策略。但是,在并发高或者任务数很多的时候,为了系统稳定性,选择合适的拒绝策略就非常重要了,如果选择不当,甚至可能会出现系统奔溃的情况。

jdk默认提供的四种拒绝策略

在jdk中,线程池的拒绝策略就是实现了 RejectedExecutionHandler 接口的实现类。当线程达到最大容量或者超过最大线程数(队列已满),执行RejectedExecutionHandler接口的rejectedExecution方法。通常配置方法有两个:

  1. 通过构造方法配置
// handler 即为拒绝策略
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)
  1. 设置拒绝策略
public void setRejectedExecutionHandler(RejectedExecutionHandler handler) 

JDK默认提供的四种策略:

ThreadPoolExecutor.AbortPolicy 中止策略

线程池默认的拒绝策略就是中止策略。中止策略会在执行器添加任务时抛出一个RejectedExecutionException 运行时异常。

// 简单任务类
static class SimpleTask implements Runnable{

    private String taskName;

    public SimpleTask(String taskName) {
        this.taskName = taskName;
    }
    public String getTaskName(){
        return this.taskName;
    }
    @Override
    public void run() {
        System.out.println(taskName  + "开始执行," + "执行线程" +  Thread.currentThread().getName());
        try {
            Thread.sleep(1 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(taskName+ "结束");
    }
}
// 演示
System.out.println("中止策略演示");
threadPoolExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

for (int i = 0; i <= 3; i++) {
    SimpleTask simpleTask = new SimpleTask("中止策略任务" + i);
    try {
        threadPoolExecutor.execute(simpleTask);
    } catch (RejectedExecutionException rejectedExecutionException) {
        System.out.println(simpleTask.taskName + "被丢弃");
    }
}
// 控制台输出如下:

中止策略演示
中止策略任务0开始执行,执行线程pool-1-thread-1
中止策略任务2开始执行,执行线程pool-1-thread-2
中止策略任务3被丢弃
中止策略任务0结束
中止策略任务1开始执行,执行线程pool-1-thread-1
中止策略任务2结束
中止策略任务1结束

其底层实现源码就是抛出一个异常

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}
ThreadPoolExecutor.DiscardPolicy 丢弃策略

丢弃策略会在提交任务失败时默默地把任务丢弃掉,失败就失败,完全不管它。

其底层实现源码就是啥也不干

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
ThreadPoolExecutor.DiscardOldestPolicy 丢弃最老任务策略

丢弃最老任务策略,就是移除任务队列的队头元素,然后提交新的任务。队头元素为什么是最老的呢?因为我们提交线程池的顺序是核心线程中,再到队列中,队头元素就是我们最先提交未执行的任务,它就是目前等待最长时间也是最老的任务。

其底层源码就是移除队头元素。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        e.getQueue().poll();
        e.execute(r);
    }
}

值得注意的是,通常我们丢弃最老任务策略不会与优先队列同时工作,因为优先队列最重要的任务就是队头元素,我们不应该丢弃。还有一点,当使用该丢弃策略时,任务队列不能是SynchronousQueue,因为它不能移除队头元素,因而不能达到拒绝新加的任务,导致一直进入死循环,抛出StackOverflowError错误

ThreadPoolExecutor.CallerRunsPolicy 调用者执行策略

调用者执行策略,当线程池线程数满时,它不再丢给线程池执行,也不丢弃掉,而是自己线程来执行,把异步任务变成同步任务。

底层源码实现:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();
    }
}

如何选择拒绝策略

没有最佳实践,只有更适合自身业务的策略。现在我们聊聊各种策略的适用场景

  1. AbortPolicy 中止策略,线程池默认的拒绝策略,也是我们最常用的拒绝策略。当系统线程池满载的时候,可以通过异常的形式告知使用方,交由使用方自行处理。一般出现此异常时,我们可以提示用户稍后再试,或者我们把未执行的任务记录下来,等到适当时机再次执行。

  2. DiscardPolicy 丢弃策略,一般我们都不会选择它,因为它直接就把任务丢弃掉了,我们毫无感知。如果任务不重要,丢弃掉也没有没关系,就可以使用它。还有一种情况,我们也可以使用它,我们事后知道哪些任务没有执行,说明任务是被丢弃了,需要重新执行。

  3. DiscardOldestPolicy 丢弃最老任务策略,如果有这种业务场景:需要淘汰等待时间最长任务,就可以适用该策略。

  4. CallerRunsPolicy 调用者执行策略。为了保证所有任务都能执行,可以使用该策略。但是它也隐藏着非常大的风险。

    比如,我们在SpringWeb项目中,有一个web请求过来需要处理一个异步任务,正常情况下,我们是交由线程池来处理任务的,但是由于线程池满了,我们使用了CallerRunsPolicy策略,该异步任务就由web请求线程来处理。

    看起来好像没有什么问题,但实际情况是,web请求已经使用了tomcat的线程池中的线程来处理的了,异步任务也交由该线程处理,此时的线程资源就被此次的web请求长久占用了。如果这样的web请求有很多,Tomcat的可用线程将会变得很少,这导致整个服务器的qps大大降低,甚至系统奔溃。

    所以使用CallerRunsPolicy策略时,要站在更高的角度来评估,这会不会给系统带来其他问题!