java线程池拒绝策略使用实践

线程池是开发过程中使用频率较高的一个并发组件之一,本篇会结合踩刀哥之前的实践经验来分享一下线程池拒绝策略的真实使用场景,至于线程池内部原理只会简单介绍,有需要的可以自行上网学习。

线程池工作机制

这里用一个例子来描述下线程池的工作机制,2015年公司boss创立公司,创立初期公司业务比较少,boss一个人(corePoolSize=1)干的有条不紊,没过多久,业务量上来了,他一个人干不过来,分身乏力,那怎么办呢?其实很简单,排队呗,就这样boss将待办的任务都添加到需求池(BlockingQueue)里面,boss又开始愉快的工作,但是客户的耐心终归有限,过了几天发现自己交给我们公司的业务还没完成,客户一气之下打电话给boss“我的活你干完没有,没干的话就停下来(shutdown/shutdownNow)吧,我找别人了”,这时候boss慌了,流着泪点上一根烟,在网上发了招聘,就这样干活的人又多了起来(addWorker),但是员工终归不是无限的,当活太多的时候,boss还是会拒绝接一些活(RejectedExecutionHandler)。公司在boss的带领下沉浮五载,本以为2020年可以大干一场,却偏偏赶上了新冠,复工日期一拖再拖,客户需求一少再少,唯独公司养的员工没少,这是公司目前最大的开支了。长痛不如短痛,boss们研究了一个政策,如果员工一个月(keepAliveTime=一个月)没有活干,那么就会被辞退(空闲线程被清理),一段时间以后不少员工被辞退了,只剩下核心人员。
画个简图帮助理解,如下:

主角登场

之前的铺垫都是为了引出RejectedExecutionHandler,现在我们来聊聊RejectedExecutionHandler的真实使用场景,先看看RejectedExecutionHandler的定义。


/**
 * A handler for tasks that cannot be executed by a {@link ThreadPoolExecutor}.
 *
 * @since 1.5
 * @author Doug Lea
 */
public interface RejectedExecutionHandler {

    /**
     * Method that may be invoked by a {@link ThreadPoolExecutor} when
     * {@link ThreadPoolExecutor#execute execute} cannot accept a
     * task.  This may occur when no more threads or queue slots are
     * available because their bounds would be exceeded, or upon
     * shutdown of the Executor.
     *
     * <p>In the absence of other alternatives, the method may throw
     * an unchecked {@link RejectedExecutionException}, which will be
     * propagated to the caller of {@code execute}.
     *
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

很显然它是一个接口,只有一个方法rejectedExecution,当线程池拒绝接受任务的时候会调用它,RejectedExecutionHandler一般由构造ThreadPoolExecutor对象的时候传入,如果没有传入会默认使用AbortPolicy。

jdk目前已提供四种RejectedExecutionHandler的实现供开发者使用,大多数情况下已够用,少数情况下用户可以选择自定义,jdk提供的四种RejectedExecutionHandler实现如下:

1.AbortPolicy:中止策略,抛出RejectedExecutionException异常由使用者处理;

2.CallerRunsPolicy:占用调用者的线程来执行被拒绝的任务;

3.DiscardOldestPolicy:将最早入队列的任务丢弃,然后重新提交被拒绝的任务(这里有可能依然不成功);

4.DiscardPolicy:抛弃策略,简单的抛弃,和AbortPolicy比较相似,区别是前者对于用户无感知;

实践场景之-AbortPolicy

在踩刀哥过往的工作中有这么一个需求,用户支付以后给用户push消息,这里就用到了线程池来处理这块业务,伪代码如下:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(...);
    try{
      threadPoolExecutor.execute(new Runnable() {
        @Override
        public void run() {
          //1. 调用推送服务push msg
        }
      });
    }catch (Exception RejectedExecutionException){
      //2. 记录日志
    }

前面说过,如果构造ThreadPoolExecutor时没有传递RejectedExecutionHandler,jdk默认会使用AbortPolicy,它内部会抛出RejectedExecutionException,所以调用者需要捕获这个异常做相应的处理,因为当时1.0的需求比较简单,所以只是简单了记录了日志,后来产品提出对于这种失败的情况需要做补偿,进而引出下面的第二个使用场景。

实践场景之-自定义RejectedExecutionHandler

前面提到产品希望对于这种被拒绝的push任务需要做补偿,具体的补偿逻辑为:如果当时被拒绝了,那就每隔2s重试一次,一共重试2次。我当时的处理措施是,如果execute失败了那就将任务放到redis中,异步取出重试,代码怎么写呢,第一版是这么写的:


ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(...);
    try{
      threadPoolExecutor.execute(new Runnable() {
        @Override
        public void run() {
          //1. 调用推送服务push msg
        }
      });
    }catch (Exception RejectedExecutionException){
      //2. 将任务添加到redis中
    }
    
    //3 定时任务扫描redis,然后添加到threadPoolExecutor中

看着确实也没有问题,也能实现功能,但是这种写法显得不太优雅,ThreadPoolExecutor对于拒绝处理这块采用了策略设计模式来优化代码,让逻辑更清晰,而我现在的写法将任务处理和拒绝处理揉在了一起,违背了原来的设计,所以决定进行改造,改造后如下:


ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( ..., new RejectedExecutionHandler() {
    @Override
      public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
        //1. 记录日志
        log.warn...
        //2 将task插入的redis中
        redis.lpush...
       
      }
});

拒绝处理的逻辑被封装到自定义的拒绝处理器当中,逻辑更清晰,表达能力更强。

实践场景之-CallerRunsPolicy

之前做过一个http推送平台,大体工作流程如下:

1.生产者将推送任务插入数据库中;

2.推送平台起一个异步线程去获取待推送任务;

3.将第2步中得到的推送任务丢到线程池里面去推送。

简单来说就是一个生产者消费者模型,推送的时候发现某些下游的接口响应时间较长,经常将线程池占满,所以就希望DelayQueuePollingTask这个线程能感知到这一情况,当线程池满的时候停止去数据库获取待推送任务,所以就将RejectedExecutionHandler设置为CallerRunsPolicy,现在可以达到如下效果:

1.生产者将推送任务插入数据库中;

2.推送平台起一个异步线程去获取待推送任务;

3.将第2步中得到的推送任务丢到线程池里面去推送;

4.线程池如果满就由DelayQueuePollingTask这个Thread自己执行推送任务,这样就可以停止去数据库获取待推送任务,DelayQueuePollingTask也不至于闲着没事,还可以分担任务。

posted @ 2022-04-22 16:26  piaobodeyun0000  阅读(177)  评论(0编辑  收藏  举报