这是Java中常用的另外一个线程池,主要用于实现任务的延迟执行及周期性执行.
听起来与Timer
很相似,但是比Timer
更加健壮,灵活一些。
官方注释翻译:
一个可以延迟执行命令,或者周期性执行命令的
ThreadPoolExecutor
. 如果需要多个工作线程,这个类就比Timer更加好用了,或者当你需要比Timer更加灵活,健壮的线程池时,也使用这个类.延迟任务在他们可用之后很快被执行,但是不完全保证实时. 任务被严格按照FIFO的顺序进行调度。
当一个提交的任务在执行前被取消了,执行就不会进行了. 默认情况下,一个取消了的任务在他的延迟时间到达之前,不会从工作队列中移除. 因为没有启用一些检查和监控,这会导致保留了过多的已取消任务。为了避免这种情况,使用
setRemoveOnCancelPolicy
方法来让任务在取消时立即从工作队列中移除.不同的执行,可能会使用不同的工作线程.
因为这个类继承了
ThreadPoolExecutor
. 一些继承的方法没有用.和
ThreadPoolExecutor
一样,如果没有特殊情况,这个类也使用Executors.defaultThreadFactory
来创建线程,也使用ThreadPoolExecutor.AbortPolicy
作为默认的拒绝策略.注意事项: 这个类重写了
execute
和submit
方法,去生成内部的ScheduledFuture
对象,以此来控制每个任务的延时和调度. 所有重写了这两个方法的子类,必须要调用父类的方法。
简单地说,这个类继承自ThreadPoolExecutor
,父类有的他都有。 除此之外.添加了对任务的延迟执行及周期性执行。
来看看实现~.
为了实现延迟及周期性执行,实现了一个基于FutureTask
的任务结构.
这个任务类,继承自FutureTask
,同时实现了RunnableScheduleFuture
接口.
可以看到,除了FutureTask
父类的属性外,额外保存了延迟时间和周期时间的参数,而由于周期性任务是要被不断的执行的,因为使用outerTask
来保存实际的任务.
提供了三个构造方法,全是赋值型的方法重载,没啥说的.
由于在线程池内部的优先级队列中,需要一个优先级. 这里通过重写compareTo
方法,实现了优先级的定义.
优先级使用三个属性定义:
所以说优先级队列的队首,放的永远是下一个要被执行的任务.
也许和第二名的执行时间一样,但是序列号晚,或者提交时间晚.
这里重写了FutureTask
的run
方法,为了支持周期性的一个属性设置.
有三个分支:
run
执行一次即可。其中涉及到另外两个方法:
设置下一次的执行时间,对于周期性任务来说,下一次执行时间就是当前时间+周期时间
。
将当前任务再次入队,等待调度执行.
如果当前状态没问题,就将任务放入工作队列,然后确保有工作线程是活着的.
否则就取消任务.
调用父类cancel
取消掉当前任务,如果需要立即移除掉工作队列中的任务,就移除.
这个类没有什么属性,完全应用了父类的属性,因此构造方法也是对父类参数的一些赋值操作.
提供了4个构造方法,对于线程工厂
和拒绝策略
这两个值,与父类并没有什么不同。
值得注意的是,所有的工作队列,都使用了DelayedWorkQueue
,这是一个特意实现的内部类.
作为一个工作队列
,他实现了队列和阻塞队列的接口,这两个在本文不详细介绍,认为大家都有所了解.
作为一个队列,最重要的就是入队出队两个方法.
提供了三个入队的方法:
可以看到,三个方法调用的都是底层的offer
实现.
需要注意:
由于所有任务的入队,都是ScheduledThreadPoolExecutor
类中自己进行的,因此认为是可信的. 所以工作队列是一个无界的队列,所有入队操作,不会超时,不会阻塞,绝对会成功.
入队方法比较粗暴,主要分为两个分支:
CompareTo
的值,将任务放在合适的位置上,以符合优先级队列的特性.出队系列复杂一点,分开讲.
直接返回了队首的元素. 不判断是否到了执行时间,因为没有弹出,只是返回一下,让调用方自己看看.
poll方法是核心的弹出队首的方法.这里就要判断时间了。
带有超时时间的版本,以阻塞等待队首元素的成功弹出.
分为两类,一种是只延迟执行,没有周期执行。
ScheduleFutureTask
. 具体的参数有. delayedExecute
进行延迟执行.约等于直接将任务放进工作队列中.
有两个版本,
这里以第二个为例。
也是首先计算参数,之后延迟执行一次,与上面的不进行周期执行的区别就是,这个方法会算出一个周期时间,然后传递给任务.
有没有发现JDK这些代码的一个特点,很多基础设施,方法
特别复杂,支持多种情况,而顶层的API很多都只是简单的转发调用等等. 在我理解,这其实是一种良好设计的体现,充分应对了可能出现需求扩展.
比如在上方的调度方法中,作为最核心的方法,却只是简单计算参数,传递给底层的队列,基本就完成了。
本文并不算特别细致,对很多细节没有深究,这里尝试从原理上总结下ScheduledThreadPoolExecutor
。
首先,它是一个ThreadPoolExecutor
。因此之前学习的,所有线程池的属性,他都有,什么线程工厂,拒绝策略,核心线程,最大线程,线程活跃时间等等特性,他都有。
他额外实现了延迟执行与周期性执行两个特点.依赖什么实现的呢?
众所周知,线程池内部有一个工作队列,这个类实现了自己DelayWorkQueue
。
ScheduledFutureTask
配合实现,提供compareTo
。poll
方法,在队首(最早触发的任务)还没有到达触发时间时, 无法从队列弹出任务去执行.FutureTask
有很多实现方法,对于调度来说,什么最重要. 这个类实现了自己的ScheduledFutureTask
.
compareTo
方法,两个任务之间要能够计算优先级。FutureTask
是一次性的,在done
之后,内部的状态已经是完成,不能再次执行了.ScheduledFutureTask
方法调用的是父类的runAndReset
方法,可以执行完一次后,将状态重置,等待下一次的调用.由以上两点,结合父类的ThreadPoolExecutor
,实现了一个可以延迟执行及周期性执行的线程池.
接下来模拟一个任务的全生命周期。懒得画图,手写吧.
time
是十分钟后,period
是一分钟.poll
方法获取任务,由于队列中只有一个任务,且没到时间,拿不到,继续等待.10+1
,下一次执行时间在11分钟. 再次将这个任务放入工作队列中.…太简单了,我们再加一个任务.
20+0.5=20.5
, 新的任务的触发时间是20分30秒. 放入队列中,此时,任务2优先级高于任务1,放到了队列的第一个.之后,每个半分钟,也就是30秒,都会执行一次任务2.每个整分钟,都会任务1和任务2各自执行一次。
完美运行.
最后这块流程梳理,主要是为了方便自己理解,如果写的过于抽象,而屏幕前的你已经理解的比较透彻了,可以不用看~.
完。
最后,欢迎关注我的个人公众号【 呼延十 】,会不定期更新很多后端工程师的学习笔记。 也欢迎直接公众号私信或者邮箱联系我,一定知无不言,言无不尽。
以上皆为个人所思所得,如有错误欢迎评论区指正。
欢迎转载,烦请署名并保留原文链接。
联系邮箱:huyanshi2580@gmail.com
更多学习笔记见个人博客或关注微信公众号 <呼延十 >——>