ScheduledThreadPoolExecutor源码阅读

1概述

      ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,同时还实现了ScheduledExecutorService接口,实现scheduleAtFixedRate、scheduleWithFixedDelay和schedule方法。我们平时使用ScheduledThreadPoolExecutor时最常用的方法就是scheduleAtFixedRate和schduleWithFixedDelay。

1.1类架构

 

调用添加任务的四个函数都会走一下流程:

  1. 如果线程池处不处于RUNNING状态,拒绝任务
  2. 创建一个对应的任务加入延时任务队列
  3. 如果工作线程小于corePoolSize,或者如果设置corePoolSize为0且工作线程数等于0,就创建一个没有firstTask的工作线程

 

值得注意的是从周期线程池构造函数来看,设置的maxPoolThreadSize默认固定等于Integer.MAX_VALUE,通过代码分析来看,ScheduledThreadPoolExecutor始终不会创建超过corePoolSize个工作线程(除非你设置的corePoolSize为0,这在ThreadPoolExecutor构造函数中是允许的)。maxPoolThreadSize参数设置为Integer.MAX_VALUE我想只有一个原因,就是为了corePoolSize的设置始终不会超过maxPoolSize个吧。

1.2属性成员

1.2.1 ScheduledFutureTask

 

        内部类ScheduledFutureTask,可以看到它继承了FutureTask类和RunableScheduledFuture接口。

        继承FutureTask的原因就是利用FutureTask的状态机来区分管理周期任务和非周期任务,非周期任务执行一次状态为结束状态(正常结束或异常),后面就会被取消执行并移除任务队列,周期任务则会执行完FutureTask后重置其状态,计算下次执行时机后重新放入任务队列。

        间接实现了Delayed接口,这是任务可以存放在DelayQueue的必要条件,因为SchduledThreadPoolExecutor类就是使用DelayQueue来存放任务实现延时执行的。从类名就可以知道这是一个支持周期运行的future任务,下面我们分析一下本类的设计实现。其中FutureTask分析请见对应文档。

 

                                                                        ScheduledFutureTask UML图

1.2.1.1类成员与成员方法

  1. sequenceNumber 任务序列号,每个任务原子的按照实例化任务依次加一,用于compareTo方法中两个任务延迟时间相同时的情况做比较使用。
  2. time 任务执行延时时间,单位为纳秒。
  3. period 正值表示固定速率执行,负值表示固定延迟执行,值0表示非重复任务。
  4. outerTask 注释中说明,重入队列的真正的任务,其实就是待执行周期任务本身,默认值为this,因为ScheduleThreadPoolExecutor类中scheduleAtFixedRate和scheduleWithFixedDelay方法调用decorateTask未加工周期任务,直接返回的任务本身,所以ScheduleThreadPoolExecutor类中outerTask就是表示任务本身this。
  5. heapIndex 记录延时任务队列数组下标,用于快速取消任务。
  6. getDelay(TimeUnit unit):long

实现父接口Delayed的方法,获得当前任务距离现在时刻的时间,时间单位需要传入。

     7. compareTo(Delayed other):int

实现两个Delayed接口的compareTo函数,因为Delayed接口继承Compare接口。大致处理流程如下:

                                                                            CompareTo流程图

​​

        因为每个ScheduledFutureTask都有唯一一个对象标识sequenceNumber,这个标识按照任务实例化先后依次递增,所以比较时有个特殊处理的分支,如果比较对象other为ScheduledFutureTask对象时,如果两者time相等则比较它们的sequenceNumber。

     本方法目的是在延迟队列中取任务执行的顺序为:延迟时间小的先执行,延迟时间一致的任务先创建的先执行。

     8. isPeriodic()

        返回是否是周期任务,具体操作就是看period是否非零。

     9. setNextRunTime()

       任务执行完成后调用此方法,设置下次运行此任务的时间。比较有意思的是本类中使用period的正、负、零来判断任务类型,如果为正则是固定周期任务(连续执行之间的时间间隔),如果为负则为固定延时任务(固定延迟为一次执行终止与下一次执行开始之间的延迟),如果为零则为单次任务。

可以从上述描述知道固定周期为两次开始执行间隔时间,而固定延时为本次结束与下次开始之间的间隔时间。

​ ​​ 事实上本方法的处理如下简单描述如下:

               (1) 如果是固定周期任务:nextRunTime = 本次runTime + delay

               (2) 如果是固定延时任务:nextRunTime = now() + delay;

      10. cancel(boolean mayInterruptIfRunning)

 

       

       调用父类FutureTask的cancel(boolean mayInterruptIfRunning)方法取消任务执行,函数参数表示是否允许中断任务执行。并且根据是否设置了“任务被取消就从任务队列中移除任务”来决定是否从任务队列中移除任务。

       removeOnCancel为ScheduledThreadPoolExecutor的一个boolean私有成员,默认为false,表示任务被取消后是否应该从任务队列中移除,只有调用线程池的setRemoveOnCancelPolicy(boolean value)方法才能设置它的值。

    11. run()

​​​https://img-blog.csdnimg.cn/20190211224233270.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTM3NTkxMzQ=,size_16,color_FFFFFF,t_70 ​​

         为了周期任务重入队列的操作,覆盖       FutureTask中的run()方法,可以看到其中还是调用了FutureTask中的run()方法和runAndReset()方法。想要真正看懂还要先看懂FutureTask的实现,具体请看FutureTask的分析文章。

 

            (1) 任务被取消,结束;canRunInCurrentRunState方法会判断调用线程池shutdown()方法后是否应该执行此任务。

            (2) 非周期任务,调用父类FutureTask的run()方法,结束

            (3) 周期任务,调用父类FutureTask的runAndReset()方法,方法失败结束;方法成功设置任务下次执行时间,重新将任务放入任务队列,结束

1.2.2 DelayedWorkQueue

 

        ThreadPoolExecutor保存任务使用BlockingQueue,本队列也必须要实现这个接口,其实为了自身功能也要实现这个接口,事实上concurrent包内有个DelayQueue也是继承AbstractQueue和实现BlockingQueue接口,不同的是DelayQueue内部使用PriorityQueue存储队列成员,而DelayedWorkQueue使用自己实现的队列。

 

        从注释可以看到为什么不用concurrent.PriorityQueue来存储队列成员,而是为了RunnableSchduledFuture接口类型重写一个优先级队列。两者的实现都是优先级队列,而如果是放入队列的是ScheduledFutureTask对象,则为它维护heapindex参数,contains()方法可以在O(1)的时间复杂度返回结果,remove也可以在O(log(n))的时间复杂度返回结果,这样就大大加快了任务被取消时从队列删除它的操作。

        ScheduledThreadPool从任务队列中取任务使用父类ThreadPoolExecutor的getTask()方法(具体ThreadPoolExecutor分析见对应文档),getTask()会调用任务队列的两个方法来获取任务,poll(long timeout, TimeUnit unit)方法和take()方法;当线程是超过corePoolSize的线程或允许corePoolSize内的线程被淘汰时,线程池判断当前工作线程达到被淘汰条件,就会调用pool(long timeout, TimeUnit unit)方法,否则调用take()方法。

1. pool(long timeout, TimeUnit unit)

        简单来说本方法功能如下所述:

        从任务队列中阻塞的取首个任务,直到达到任务的执行时机返回任务,或者未达到任务执行时机发生超时返回null。

2. take()

         简单来说本方法功能如下所述:

         从任务队列阻塞的取首个任务,直到达到任务的执行时机返回任务。

         任务的周期执行就是靠这个优先级阻塞队列,每次阻塞到任务执行时机取出任务执行,执行完毕后重置任务状态,设置任务下次执行时机放入任务队列中等待下次执行。

1.3构造函数

 

共有四个构造函数,通过对缺省参数设置默认值定制想要的schedule线程池。

        构造函数直接调用父类ThreadPoolExecutor的构造函数,本类重载实现了四个构造函数,其中参数corePoolSize必需设置,其他参数不设置采用传入默认值以及调用ThreadPoolExecutor不同构造函数的方式。其中ThreadPoolExecutor构造函数中maximumPoolSize、keepAliveTime、workQueue参数不允许传值,maximumPoolSize使用默认参数Integer.MAX_VALUE,keepAliveTime默认传入0,workQueue默认传入DelayedWorkQueue。

 

       

        DelayedWorkQueue是ScheduledThreadPoolExecutor类中声明的静态内部类,它和DelayQueue一样继承AbstractQueue和BlockingQueue,但是内部队列的实现不同,用于线程池任务的存取。

1.4源码分析

1.4.1 scheduleAtFixedRate

 

创建并执行一个周期任务,这个任务在给定的初始延迟后按照给定周期一直周期执行。

参数:

  1. Runnable command,周期任务
  2. long initialDlay,第一次执行任务的延迟时间
  3. long period,周期
  4. TimeUnit unit,时间单位

        这个方法通常用的最多,但是很容易和scheduleWithFixedDelay方法搞混。使用这个方法会以固定周期启动任务的执行,也就是本次执行任务开始时刻和下次执行任务开始时刻之间是固定周期的;而scheduleWithFixedDelay方法则是本次任务执行完成后固定延迟一段时间开始下次任务的执行,即本次执行任务完成时刻和下次执行任务开始时刻之间是固定延迟的。它们两个之间的差异就是在计算任务下次执行时机的地方有差异。事实上它们两个的实现基本完全一模一样,仅仅是用SchduledFutureTask.period的正负来区分是那种任务。

        这种说法只是在“工作线程足够多到不会任务达到了执行时机,但是没有工作线程去执行它”的情况。因为SchduledThreadPoolExecutor并不会创建超过corePoolSize个工作线程,除非你设置的corePoolSize为0,这时工作线程最多为1。

请看下面例子:

 

        这段代码创建一个corePoolSize为1的SchduledThreadPoolExecutor,并使用scheduledAtFixedRate创建两个固定周期为5秒的定时任务,任务执行10秒。它的执行结果如下:

 

       

        可以看到同一个周期任务两次执行开始的时间间隔为20秒,而不是5秒,因为只有一个线程池来执行这两个周期任务,导致周期任务每次执行完毕重新计算下次执行时间,并放回任务队列后,才能去任务队列中取下一个任务执行。

        当然换成scheduleWithFixedDelay方法来执行这段代码,也不会有“同一任务结束后延迟固定时长启动下次执行的结果”。执行结果如下:

 

        方法的流程很简单,就是创建一个SchduledFutureTask,并且将任务加入DelayedWorkQueue中。主要处理流程都在delayedExecute方法中实现,这个方法的分析请在下文中寻找。

 

实现对比图

 

1.4.2 scheduleWithFixedDelay

 

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==
看1.4.1 scheduleAtFixedRate所述。

 

1.4.3 schedule(Runnable command,long delay, TimeUnit unit)

 

 

        函数声明中表述:这个函数用给定初始delay创建一个一次性执行的任务。

        它的实现和上面两个函数基本也都一样,不同的是实例化一个SchduledFutureTask使用的构造函数不同,这个构造函数直接设置任务的period为0,即表示这个任务为一次性任务,执行过这个任务不会重置它的状态再加入任务队列。

1.4.4 schedule(Callable<V> callable, long delay, TimeUnit unit)

 

        接口中的函数声明未表示创建的是一次性任务,但是事实上ScheduledThreadPoolExecutor中的实现也是创建一个一次性执行的延时任务,它与上一个函数的区别就是入参使用Callable规定了任务的返回结果类型,尽管实现中并不关心;而上一个函数使用Runnable,result设置为null。所以它们两个的唯一区别也就是入参不同。

1.4.5 delayedExecute(RunnableScheduledFuture<?> task)

        到此时这个方法的出现率是最高的,因为ScheduledThreadPoolExecutor对外暴露的上述四个最重要的方法中都是调用它来实现的。

        方法注释:如果线程池关闭则拒绝任务,否则添加任务到任务队列,加入成功后随即再次判断线程池状态,如果线程池关闭,则判断是否允许任务在线程池关闭后仍然执行,如果不是则需要从任务队列移除任务,并且取消任务。如果线程池还要继续执行任务(线程池处于RUNNING或线程池被关闭但是任务仍然要执行),确认线程池中工作线程数,如果觉得不够添加一个(处理流程在父类定义的ensurePrestart()方法中)。

        这个方法主要做以下的事情:

  1. 如果线程池状态不是RUNNING,直接执行拒绝策略
  2. 如果线程池处于RUNNING状态,添加任务到任务队列
  3. 再判断线程池状态,如果线程池状态是SHUTDOWN,判断线程池是否设置了“线程池状态为SHUTDOWN时需要继续执行任务队列的任务”,如果不需要执行,就从任务队列中移除改任务,并取消该任务。如果设置了“线程池状态为SHUTDOWN时需要继续执行任务队列的任务”,即线程池尽管是SHUTDOWN状态,但是仍然要继续任务队列的任务,那么会调用ensurePreStart(),判断是否需要补充工作线程(即worker)

 

private void delayedExecute(RunnableScheduledFuture<?> task) {

    if (isShutdown())//线程池被shutdown拒绝任务

        reject(task);

    else {

        super.getQueue().add(task);//添加任务到任务队列

        if (isShutdown() &&

            !canRunInCurrentRunState(task.isPeriodic()) &&

            remove(task))

            task.cancel(false);//符合上述三个顺序判断条件,判断无需执行这个任务,
                               //取消任务

        else

            ensurePrestart();//补充工作线程

    }

}

ensurePreStart()方法逻辑简单,主要就两步:

  1. 如果当前worker数小于corePoolSize个,补充一个worker
  2. 如果corePoolSize被设成了0,线程池当前没有worker,那么补充一个worker

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值