介绍
自JDK1.5开始,JDK提供了ScheduledThreadPoolExecutor类来支持周期性任务的调度。在这之前的实现需要依靠Timer和TimerTask或者其它第三方工具来完成。但Timer有不少的缺陷:
- Timer是单线程模式;
- 如果在执行任务期间某个TimerTask耗时较久,那么就会影响其它任务的调度;
- Timer的任务调度是基于绝对时间的,对系统时间敏感;
- Timer不会捕获执行TimerTask时所抛出的异常,由于Timer是单线程,所以一旦出现异常,则线程就会终止,其他任务也得不到执行。
ScheduledThreadPoolExecutor继承ThreadPoolExecutor来重用线程池的功能,它的实现方式如下:
- 将任务封装成ScheduledFutureTask对象,ScheduledFutureTask基于相对时间,不受系统时间的改变所影响;
- ScheduledFutureTask实现了
java.lang.Comparable
接口和java.util.concurrent.Delayed
接口,所以有两个重要的方法:compareTo和getDelay。compareTo方法用于比较任务之间的优先级关系,如果距离下次执行的时间间隔较短,则优先级高;getDelay方法用于返回距离下次任务执行时间的时间间隔; - ScheduledThreadPoolExecutor定义了一个DelayedWorkQueue,它是一个有序队列,会通过每个任务按照距离下次执行时间间隔的大小来排序;
- ScheduledFutureTask继承自FutureTask,可以通过返回Future对象来获取执行的结果。
通过如上的介绍,可以对比一下Timer和ScheduledThreadPoolExecutor:
Timer | ScheduledThreadPoolExecutor |
---|---|
单线程 | 多线程 |
单个任务执行时间影响其他任务调度 | 多线程,不会影响 |
基于绝对时间 | 基于相对时间 |
一旦执行任务出现异常不会捕获,其他任务得不到执行 | 多线程,单个任务的执行不会影响其他线程 |
所以,在JDK1.5之后,应该没什么理由继续使用Timer进行任务调度了。
ScheduledThreadPoolExecutor的使用
下面用一个具体的例子来说明ScheduledThreadPoolExecutor的使用:
|
|
下面就来具体分析一下ScheduledThreadPoolExecutor的实现过程。
ScheduledThreadPoolExecutor的实现
ScheduledThreadPoolExecutor的类结构
看下ScheduledThreadPoolExecutor内部的类图:

不要被这么多类吓到,这里只不过是为了更清楚的了解ScheduledThreadPoolExecutor有关调度和队列的接口。
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,实现了ScheduledExecutorService接口,该接口定义了schedule等任务调度的方法。
同时ScheduledThreadPoolExecutor有两个重要的内部类:DelayedWorkQueue和ScheduledFutureTask。可以看到,DelayeddWorkQueue是一个阻塞队列,而ScheduledFutureTask继承自FutureTask,并且实现了Delayed接口。有关FutureTask的介绍请参考另一篇文章:FutureTask源码解析。
ScheduledThreadPoolExecutor的构造方法
ScheduledThreadPoolExecutor有3中构造方法:
|
|
因为ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以这里都是调用的ThreadPoolExecutor类的构造方法。有关ThreadPoolExecutor可以参考深入理解Java线程池:ThreadPoolExecutor。
这里注意传入的阻塞队列是DelayedWorkQueue类型的对象。后面会详细介绍。
schedule方法
在上文的例子中,使用了schedule方法来进行任务调度,schedule方法的代码如下:
|
|
首先,这里的两个重载的schedule方法只是传入的第一个参数不同,可以是Runnable对象或者Callable对象。会把传入的任务封装成一个RunnableScheduledFuture对象,其实也就是ScheduledFutureTask对象,decorateTask默认什么功能都没有做,子类可以重写该方法:
|
|
然后,通过调用delayedExecute方法来延时执行任务。
最后,返回一个ScheduledFuture对象。
scheduleAtFixedRate方法
该方法设置了执行周期,下一次执行时间相当于是上一次的执行时间加上period,它是采用已固定的频率来执行任务:
|
|
scheduleWithFixedDelay方法
该方法设置了执行周期,与scheduleAtFixedRate方法不同的是,下一次执行时间是上一次任务执行完的系统时间加上period,因而具体执行时间不是固定的,但周期是固定的,是采用相对固定的延迟来执行任务:
|
|
注意这里的unit.toNanos(-delay));
,这里把周期设置为负数来表示是相对固定的延迟执行。
scheduleAtFixedRate和scheduleWithFixedDelay的区别在setNextRunTime方法中就可以看出来:
|
|
setNextRunTime方法会在run方法中执行完任务后调用。
triggerTime方法
triggerTime方法用于获取下一次执行的具体时间:
|
|
这里的delay < (Long.MAX_VALUE >> 1
是为了判断是否要防止Long类型溢出,如果delay的值小于Long类型最大值的一半,则直接返回delay,否则需要进行防止溢出处理。
overflowFree方法
该方法的作用是限制队列中所有节点的延迟时间在Long.MAX_VALUE之内,防止在compareTo方法中溢出。
|
|
当一个任务已经可以执行出队操作,但还没有执行,可能由于线程池中的工作线程不是空闲的。具体分析一下这种情况:
- 为了方便说明,假设Long.MAX_VALUE=1023,也就是11位,并且当前的时间是100,调用triggerTime时并没有对delay进行判断,而是直接返回了
now() + delay
,也就是相当于100 + 1023
,这肯定是溢出了,那么返回的时间是-925; - 如果头节点已经可以出队但是还没有执行出队,那么头节点的执行时间应该是小于当前时间的,假设是95;
- 这时调用offer方法向队列中添加任务,在offer方法中会调用siftUp方法来排序,在siftUp方法执行时又会调用ScheduledFutureTask中的compareTo方法来比较执行时间;
- 这时如果执行到了compareTo方法中的
long diff = time - x.time;
时,那么计算后的结果就是-925 - 95 = -1020
,那么将返回-1,而正常情况应该是返回1,因为新加入的任务的执行时间要比头结点的执行时间要晚,这就不是我们想要的结果了,这会导致队列中的顺序不正确。 - 同理也可以算一下在执行compareTo方法中的
long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
时也会有这种情况; - 所以在triggerTime方法中对delay的大小做了判断,就是为了防止这种情况发生。
如果执行了overflowFree方法呢,这时headDelay = 95 - 100 = -5
,然后执行delay = 1023 + (-5) = 1018
,那么triggerTime会返回100 + 1018 = -930
,再执行compareTo方法中的long diff = time - x.time;
时,diff = -930 - 95 = -930 - 100 + 5 = 1018 + 5 = 1023
,没有溢出,符合正常的预期。
所以,overflowFree方法中把已经超时的部分时间给减去,就是为了避免在compareTo方法中出现溢出情况。
(说实话,这段代码看的很痛苦,一般情况下也不会发生这种情况,谁会传一个Long.MAX_VALUE呢。要知道Long.MAX_VALUE的纳秒数换算成年的话是292年,谁会这么无聊。。。)
ScheduledFutureTask的getDelay方法
|
|
ScheduledFutureTask的构造方法
ScheduledFutureTask继承自FutureTask并实现了RunnableScheduledFuture接口,具体可以参考上文的类图,构造方法如下:
|
|
这里面有几个重要的属性,下面来解释一下:
- time:下次任务执行时的时间;
- period:执行周期;
- sequenceNumber:保存任务被添加到ScheduledThreadPoolExecutor中的序号。
在schedule方法中,创建完ScheduledFutureTask对象之后,会执行delayedExecute方法来执行任务。
delayedExecute方法
|
|
说一下这里的第二个if判断:
- 如果不是SHUTDOWN状态,执行else,否则执行步骤2;
- 如果在当前线程池运行状态下可以执行任务,执行else,否则执行步骤3;
- 从阻塞队列中删除任务,如果失败,执行else,否则执行步骤4;
- 取消任务,但不中断执行中的任务。
对于步骤2,可以通过setContinueExistingPeriodicTasksAfterShutdownPolicy方法设置在线程池关闭时,周期任务继续执行,默认为false,也就是线程池关闭时,不再执行周期任务。
ensurePrestart方法在ThreadPoolExecutor中定义:
|
|
调用了addWorker方法,可以在深入理解Java线程池:ThreadPoolExecutor中查看addWorker方法的介绍,线程池中的工作线程是通过该方法来启动并执行任务的。
ScheduledFutureTask的run方法
回顾一下线程池的执行过程:当线程池中的工作线程启动时,不断地从阻塞队列中取出任务并执行,当然,取出的任务实现了Runnable接口,所以是通过调用任务的run方法来执行任务的。
这里的任务类型是ScheduledFutureTask,所以下面看一下ScheduledFutureTask的run方法:
|
|
有关FutureTask的run方法和runAndReset方法,可以参考FutureTask源码解析。
分析一下执行过程:
- 如果当前线程池运行状态不可以执行任务,取消该任务,然后直接返回,否则执行步骤2;
- 如果不是周期性任务,调用FutureTask中的run方法执行,会设置执行结果,然后直接返回,否则执行步骤3;
- 如果是周期性任务,调用FutureTask中的runAndReset方法执行,不会设置执行结果,然后直接返回,否则执行步骤4和步骤5;
- 计算下次执行该任务的具体时间;
- 重复执行任务。
ScheduledFutureTask的reExecutePeriodic方法
|
|
该方法和delayedExecute方法类似,不同的是:
- 由于调用reExecutePeriodic方法时已经执行过一次周期性任务了,所以不会reject当前任务;
- 传入的任务一定是周期性任务。
onShutdown方法
onShutdown方法是ThreadPoolExecutor中的钩子方法,在ThreadPoolExecutor中什么都没有做,参考深入理解Java线程池:ThreadPoolExecutor,该方法是在执行shutdown方法时被调用:
|
|
DelayedWorkQueue
ScheduledThreadPoolExecutor之所以要自己实现阻塞的工作队列,是因为ScheduledThreadPoolExecutor要求的工作队列有些特殊。
DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面(注意:这里的顺序并不是绝对的,堆中的排序只保证了子节点的下次执行时间要比父节点的下次执行时间要大,而叶子节点之间并不一定是顺序的,下文中会说明)。
堆结构如下图所示:

可见,DelayedWorkQueue是一个基于最小堆结构的队列。堆结构可以使用数组表示,可以转换成如下的数组:

在这种结构中,可以发现有如下特性:
假设,索引值从0开始,子节点的索引值为k,父节点的索引值为p,则:
- 一个节点的左子节点的索引为:k = p * 2 + 1;
- 一个节点的右子节点的索引为:k = (p + 1) * 2;
- 一个节点的父节点的索引为:p = (k - 1) / 2。
为什么要使用DelayedWorkQueue呢?
定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。
DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。
DelayedWorkQueue的属性
|
|
注意这里的leader,它是Leader-Follower模式的变体,用于减少不必要的定时等待。什么意思呢?对于多线程的网络模型来说:
所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。
参考自:http://blog.csdn.net/goldlevi/article/details/7705180
具体leader的作用在分析take方法时再详细介绍。
offer方法
既然是阻塞队列,入队的操作如add和put方法都调用了offer方法,下面查看一下offer方法:
|
|
有关Condition的介绍请参考深入理解AbstractQueuedSynchronizer(三)
这里的重点是siftUp方法。
siftUp方法
|
|
代码很好理解,就是循环的根据key节点与它的父节点来判断,如果key节点的执行时间小于父节点,则将两个节点交换,使执行时间靠前的节点排列在队列的前面。
假设新入队的节点的延迟时间(调用getDelay()方法获得)是5,执行过程如下:
- 先将新的节点添加到数组的尾部,这时新节点的索引k为7:
- 计算新父节点的索引:parent = (k - 1) >>> 1,parent = 3,那么queue[3]的时间间隔值为8,因为 5 < 8 ,将执行queue[7] = queue[3]::
- 这时将k设置为3,继续循环,再次计算parent为1,queue[1]的时间间隔为3,因为 5 > 3 ,这时退出循环,最终k为3:
可见,每次新增节点时,只是根据父节点来判断,而不会影响兄弟节点。
另外,setIndex方法只是设置了ScheduledFutureTask中的heapIndex属性:
|
|
take方法
|
|
take方法是什么时候调用的呢?在深入理解Java线程池:ThreadPoolExecutor中,介绍了getTask方法,工作线程会循环地从workQueue中取任务。但定时任务却不同,因为如果一旦getTask方法取出了任务就开始执行了,而这时可能还没有到执行的时间,所以在take方法中,要保证只有在到指定的执行时间的时候任务才可以被取走。
再来说一下leader的作用,这里的leader是为了减少不必要的定时等待,当一个线程成为leader时,它只等待下一个节点的时间间隔,但其它线程无限期等待。 leader线程必须在从take()或poll()返回之前signal其它线程,除非其他线程成为了leader。
举例来说,如果没有leader,那么在执行take时,都要执行available.awaitNanos(delay)
,假设当前线程执行了该段代码,这时还没有signal,第二个线程也执行了该段代码,则第二个线程也要被阻塞。多个这时执行该段代码是没有作用的,因为只能有一个线程会从take中返回queue[0](因为有lock),其他线程这时再返回for循环执行时取的queue[0],已经不是之前的queue[0]了,然后又要继续阻塞。
所以,为了不让多个线程频繁的做无用的定时等待,这里增加了leader,如果leader不为空,则说明队列中第一个节点已经在等待出队,这时其它的线程会一直阻塞,减少了无用的阻塞(注意,在finally中调用了signal()来唤醒一个线程,而不是signalAll())。
poll方法
下面看下poll方法,与take类似,但这里要提供超时功能:
|
|
finishPoll方法
当调用了take或者poll方法能够获取到任务时,会调用该方法进行返回:
|
|
siftDown方法
siftDown方法使堆从k开始向下调整:
|
|
siftDown方法执行时包含两种情况,一种是没有子节点,一种是有子节点(根据half判断)。例如:
没有子节点的情况:
假设初始的堆如下:

假设 k = 3 ,那么 k = half ,没有子节点,在执行siftDown方法时直接把索引为3的节点设置为数组的最后一个节点:

有子节点的情况:
假设 k = 0 ,那么执行以下步骤:
- 获取左子节点,child = 1 ,获取右子节点, right = 2 :
- 由于
right < size
,这时比较左子节点和右子节点时间间隔的大小,这里 3 < 7 ,所以 c = queue[child] ; - 比较key的时间间隔是否小于c的时间间隔,这里不满足,继续执行,把索引为k的节点设置为c,然后将k设置为child,;
- 因为 half = 3 ,k = 1 ,继续执行循环,这时的索引变为:
- 这时再经过如上判断后,将k的值为3,最终的结果如下:
- 最后,如果在finishPoll方法中调用的话,会把索引为0的节点的索引设置为-1,表示已经删除了该节点,并且size也减了1,最后的结果如下:
可见,siftdown方法在执行完并不是有序的,但可以发现,子节点的下次执行时间一定比父节点的下次执行时间要大,由于每次都会取左子节点和右子节点中下次执行时间最小的节点,所以还是可以保证在take和poll时出队是有序的。
remove方法
|
|
假设初始的堆结构如下:

这时要删除8的节点,那么这时 k = 1,key为最后一个节点:

这时通过上文对siftDown方法的分析,siftDown方法执行后的结果如下:

这时会发现,最后一个节点的值比父节点还要小,所以这里要执行一次siftUp方法来保证子节点的下次执行时间要比父节点的大,所以最终结果如下:

总结
本文详细分析了ScheduedThreadPoolExecutor的实现,主要介绍了以下方面:
- 与Timer执行定时任务的比较,相比Timer,ScheduedThreadPoolExecutor有什么优点;
- ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,所以它也是一个线程池,也有coorPoolSize和workQueue,ScheduledThreadPoolExecutor特殊的地方在于,自己实现了优先工作队列DelayedWorkQueue;
- ScheduedThreadPoolExecutor实现了ScheduledExecutorService,所以就有了任务调度的方法,如schedule,scheduleAtFixedRate和scheduleWithFixedDelay,同时注意他们之间的区别;
- 内部类ScheduledFutureTask继承自FutureTask,实现了任务的异步执行并且可以获取返回结果。同时也实现了Delayed接口,可以通过getDelay方法获取将要执行的时间间隔;
- 周期任务的执行其实是调用了FutureTask类中的runAndReset方法,每次执行完不设置结果和状态。参考FutureTask源码解析;
- 详细分析了DelayedWorkQueue的数据结构,它是一个基于最小堆结构的优先队列,并且每次出队时能够保证取出的任务是当前队列中下次执行时间最小的任务。同时注意一下优先队列中堆的顺序,堆中的顺序并不是绝对的,但要保证子节点的值要比父节点的值要大,这样就不会影响出队的顺序。
总体来说,ScheduedThreadPoolExecutor的重点是要理解下次执行时间的计算,以及优先队列的出队、入队和删除的过程,这两个是理解ScheduedThreadPoolExecutor的关键。