线程池拒绝策略选型与自定义

403 阅读6分钟

一. 前言

1.1 什么是拒绝策略

线程池是业务中常用的线程调度工具,说到线程池原理,大家应该都了解过:corePoolSize,maximumPoolSize,workQueue等等。

当workThread没有空闲且workQueue已经填满时,说明线程池当前没有消费任务的能力了,那么再多出来的任务应该如何处理,这就是拒绝策略。

1.2 拒绝策略的重要性

拒绝策略被Executer包装的比较好,很多开发者都没有显示配置过拒绝策略,在并发比较低或者并发量可预期的场景下没有什么问题。

实际的业务中,在并发量超出预期时,因为拒绝策略选择不当引发的问题还不少, 比如:

  • 一批任务中某些没有执行
  • 线程池执行异常退出
  • 应用CPU突然升高

这些问题会导致系统逻辑不可控,且大多表征不明,需要调查好久才能解决问题。

1.3 JUC内置的策略

在ThreadPoolExecutor中内置了4个拒绝策略

  • AbortPolicy
  • DiscardPolicy
  • CallerPolicy
  • DiscardOldestPolicy

并且维护 defaultHandler = new AbortPolicy(); 因此当我们没有显示指定拒绝策略时,默认使用的是AbortPolicy策略。

定位到源码可以看到:这些策略都是对于RejectedExecutionHandler的实现,核心逻辑在rejectedExecution方法中。

public static class AbortPolicy implements RejectedExecutionHandler {
    /**
     * Creates an {@code AbortPolicy}.
     */
    public AbortPolicy() { }
    // r:任务,e:线程池
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

下文我们通过用例测试一下这4个策略效果和实现的原理。

二. 内置策略的测评

2.1 测评用例

  • 线程池构建
public static ThreadPoolExecutor buildPolicyPool(RejectedExecutionHandler policy) {
    return new ThreadPoolExecutor(1,
            1, 10, TimeUnit.MINUTES, new ArrayBlockingQueue<>(5),
            policy);
}

构造方法,指定核心线程数和最大线程数都为1,等待队列长度为5,参数为不同的执行策略。

  • 执行方法

方法内部创建10个线程送往待测线程池中执行。

public static void exec(ThreadPoolExecutor pool) {
    AtomicInteger taskNum = new AtomicInteger();
    for (int i = 0; i < 10; i++) {
        String taskName = "任务" + i;
        // 为了保证异常不影响整个线程池运行,加个异常捕获。
        try {
            pool.submit(() -> {
                try {
                    // 因为线程池只有一个线程,所以workThread一直是一个
                    System.out.println("threadName:" + Thread.currentThread().getName());
                    // 模拟任务执行时间长,队列满的情况
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        } catch (Exception e) {
            System.out.println(taskName + "被拒绝");
        }
    }
    // 保证主线程不停止,增加延迟时间
    try {
        Thread.sleep(100000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

2.2 AbortPolicy

  • 源码实现
public static class AbortPolicy implements RejectedExecutionHandler {

    public AbortPolicy() { }

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

仅抛出了异常,没有其他处理,为了正常运行,这里把异常catch住了。

  • 评测结果
/**
 * description:拒绝并抛出异常, Executor工厂模式生成pool的默认策略
 * expect:前6个任务正常执行,后4个任务全部拒绝抛出异常
 * analysis: 一共10个任务,coreSize = maxSize = 1,第1个任务被分配执行,然后5个依次进入等待队列,最后剩余4个被丢弃
 */
exec(buildPolicyPool(new ThreadPoolExecutor.AbortPolicy()));
threadName:pool-1-thread-1
任务6被拒绝
任务7被拒绝
任务8被拒绝
任务9被拒绝
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1

2.3 DiscardPolicy

  • 源码实现
public static class DiscardPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code DiscardPolicy}.
     */
    public DiscardPolicy() { }

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    }
}

直接丢弃,什么都没有做....

  • 评测结果
* description:默默拒绝,不抛异常
* expect:前6个任务正常执行,后四个任务全部拒绝但不抛出异常
* analysis:逻辑同上,只是不抛异常了
*/
exec(buildPolicyPool(new ThreadPoolExecutor.DiscardPolicy()));
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1

2.4 CallerPolicy

  • 源码实现
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code CallerRunsPolicy}.
     */
    public CallerRunsPolicy() { }

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

如果线程池没有down,那么就由添加到pool的线程(主线程)去执行这个任务。

  • 评测结果
/**
 * description:由调用线程池的主线程执行任务,直到线程池结束
 * expect:前6个任务由线程池线程执行,后4个任务由主线程执行2个,线程池线程执行2个
 * analysis:前6个逻辑同上,后面任务因为每个任务时间恒定,因此剩余任务相当于主线程和工作线程同步执行,没个线程执行2个任务。
 */
exec(buildPolicyPool(new ThreadPoolExecutor.CallerRunsPolicy()));
threadName:pool-1-thread-1
threadName:main
threadName:pool-1-thread-1
threadName:main
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1

2.5 DiscardOldestPolicy

  • 源码实现
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code DiscardOldestPolicy} for the given executor.
     */
    public DiscardOldestPolicy() { }

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

如果线程池没有down,从等待队列头部移除一个任务(新加的任务顺势到队尾)然后继续运行。

  • 评测结果
/**
 * description:从队列头部移除任务,让新任务能加进来继续执行,直到线程池结束
 * expect:只执行6个任务
 * analysis:前6个逻辑同上,后面的任务依次替换前面的任务,最终只执行 workThreadSize + queueSize = 6个
 */

exec(buildPolicyPool(new ThreadPoolExecutor.DiscardOldestPolicy()));
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1

三. 自定义策略实现

从上文我们可以看到JUC内置的策略不过就是实现了RejectedExecutionHandler,那我们也可以自己实现一个和业务最契合的策略。

public static LinkedBlockingQueue<Runnable> businessQueue = new LinkedBlockingQueue();

public static class MyselfPolicy implements RejectedExecutionHandler {

    // 这里简单用一个队列模拟补偿,也可以入库,发送mq等方式实现。
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        businessQueue.add(r);
        System.out.println("进入业务补偿处理队列中:" + r.toString());
    }
}

执行一下测试用例

/**
 * description:自定义队列,进入等待队列中
 * expect:成功执行前6个任务,后4个任务进入补偿队列中
 * analysis:前6个逻辑同上,后面的任务走自定义逻辑
 */
exec(buildPolicyPool(new MyselfPolicy()));
threadName:pool-1-thread-1
进入业务补偿处理队列中:java.util.concurrent.FutureTask@12781b30
进入业务补偿处理队列中:java.util.concurrent.FutureTask@16fbbf52
进入业务补偿处理队列中:java.util.concurrent.FutureTask@29674c82
进入业务补偿处理队列中:java.util.concurrent.FutureTask@9c20ebf
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1
threadName:pool-1-thread-1

四. 总结

每种拒绝策略的选型参考如下:

  • AbortPolicy

    直接丢弃抛出异常,需要调用时catch打印一个日志告警,适用于对任务结果不敏感,可以重试场景。

  • DiscardPolicy

    没有任何反应,只是丢弃了,丢失任务业务完全无感知,不建议使用。

  • CallerPolicy

    业务线程会介入任务处理,对于并发比较高的场景慎用,会出现业务线程被占用导致整个服务瘫痪。

  • DiscardOldestPolicy

    丢弃老任务,保证新任务执行,看似合理实际简单粗暴,除非业务和它的思想完全一致,否则不建议使用,出现问题很难回溯。

默认的策略无论哪种实现都比较简陋,如果要做到契合业务,还是自定义一个策略来实现更好。