java任务调度之Timer定时器
定时器相信大家都不陌生,平时使用定时器就像使用闹钟一样,我们可以在固定的时间做某件事,也可以在固定的时间段重复做某件事,今天就来分析一下java中自带的定时任务器Timer。
一、Timer基本使用
在Java中为我们提供了Timer来实现定时任务,当然现在还有很多定时任务框架,比如说Spring、QuartZ、Linux Cron等等,而且性能也更加优越。但是我们想要深入的学习就必须先从最简单的开始。
在Timer定时任务中,最主要涉及到了两个类:Timer和TimerTask。他们俩的关系也特别容易理解,TimerTask把我们得业务逻辑写好之后,然后使用Timer定时执行就OK了。我们来看一个最基本的案例:
![](http://pics3.baidu.com/feed/8694a4c27d1ed21b7b8fb6ba82c85fc150da3f52.jpeg?token=48aebce61d7faa99b037df2362a410ae&s=329131CC52F4866F5CF1A50B0000E0C1)
这就是我们的TimerTask,我们单独写成类时候需要去继承TimerTask。然后呢我们写好了之后就可以使用Timer来执行了。
![](http://pics0.baidu.com/feed/64380cd7912397dd3edcb691762430b2d1a2872d.jpeg?token=74fd2f6c74774c6eef50cb6c91b7b34a&s=B281B14CCBE09D724E65D48B00007081)
指定的流程很简单:
(1)第一步:创建一个Timer。
(2)第二步:创建一个TimerTask。
(3)第三步:使用Timer执行TimerTask。
其中第三步无疑是我们目前最关心的,也就是timer.schedule(myTask, 2000L, 1000L)。他的意思是myTask在两秒钟之后开始第一次执行,然后每隔一秒执行一次。这只是最基本的用法。就体现了Timer定时执行的流程。当然java中Timer还为我们提供了很多其他的方法。对此就有必要深入其源码看看了。
二、Timer源码分析
对于一个类的源码分析,我一贯的思路就是先从参数开始,然后构造方法,最后就是常用方法。下面我们就按照这个思路开始今天的源码分析,在这里基于jdk1.8。先给出一张整体类图:
![](http://pics3.baidu.com/feed/14ce36d3d539b60011a644c9d9f6b72fc75cb78b.jpeg?token=46229500405b957c963a3833f85dd837&s=BEA87023119F41CA18DDF4CB000080B1)
1、参数
Timer的源码中为我们提供了两个最主要的参数TaskQueue和TimerThread。
![](http://pics5.baidu.com/feed/0d338744ebf81a4cee760586f88ce25c242da661.jpeg?token=44f091ba7b9759e948c26109edf0a4b5&s=BA80F14C1BE0996044455406000070C3)
上面的代码大概意思是这样的:
(1)TaskQueue:这是一个最小堆,它存放该Timer的所有TimerTask。
(2)TimerThread:执行TaskQueue中的任务,执行完从任务队列中移除。
所以上面这两个参数其实是配合着使用的,那这个TaskQueue是如何存放的呢?在这里我们不妨跟进去看看。
![](http://pics6.baidu.com/feed/9a504fc2d562853508970e8ebf49f4c3a6ef6352.jpeg?token=b1fce73aee0437c21fad75f4115dec0f&s=BA80F14C9FE099705CE545030000E0C3)
在这里我们只给出了一部分源码,不过这一部分是整个思想原理最核心的,上面英文的大概意思是;TaskQueue是一个平衡二叉堆,具有最小 nextExecutionTime 的 TimerTask 在队列中为 queue[1] ,也就是堆中的根节点。第 n 个位置 queue[n] 的子节点分别在 queue[2n] 和 queue[2n+1] 。不了解二叉堆的话,可以看看数据结构。
也就是说TimerTask 在堆中的位置其实是通过nextExecutionTime 来决定的。nextExecutionTime 越小,那么在堆中的位置越靠近根,越有可能先被执行。而nextExecutionTime意思就是下一次执行开始的时间。
还有一个TimerTask数组,默认大小是128个。
2、构造方法
构造方法就比较简单了,这里一共有四个:
![](http://pics7.baidu.com/feed/ac4bd11373f082024c268c9b645d79e8aa641b0c.jpeg?token=4522a8cb0ddfd34910154fad2305b320&s=329031CC5AF6B64F04C9840E0000F0C1)
(1)第一个:默认构造方法。
(2)第二个:在构造器中指定是否是守护线程。
(3)第三个:带有名字的构造方法。
(3)第四个:不仅带名字,还指定是否是守护线程。
不过我们需要注意一点的是,Timer在构造完成之后会启动一个后台线程用于执行TaskQueue里面的TimerTask 。
3、定时任务方法
在一开始我们提到,我们不仅可以在指定的时间执行某些任务,还可以在一段时间之后执行。我们对这些方法进行总结一下:
(1)schedule(task,time) 在时间等于或超过time的时候执行且只执行一次task,这个time表示的是例如2019年11月11日上午11点11分11秒。指的是时刻。
(2)schedule(task,time,period)
在时间等于或超过time的时候首次执行task,之后每隔period毫秒重复执行一次task 。这个time和上一个一样。
(3)schedule(task, delay)
在delay时间之后,执行且只执行一次task。这个delay表示的是延迟时间,比如说三秒后执行。
(4)schedule(task,delay,period)
在delay时间之后,开始首次执行task,之后每隔period毫秒重复执行一次task ,这个delay和上面的一样。
我们不如来看看源码:
![](http://pics3.baidu.com/feed/faf2b2119313b07e998b043e3c71132697dd8c79.jpeg?token=2af35d0d17dabeb818f97295372519fe&s=329131CC52B4846F58F5990B000030C1)
这四个方法都执行了同一个方法sched,所以我们要弄清楚原理,就必须要再跟进去看看:
![](http://pics5.baidu.com/feed/f7246b600c338744acb14a5960a95bfcd62aa0ce.jpeg?token=be32631f782dc319b66abc714ce1e571&s=3281B14CDAA6B44D5C59F00B0000E0C1)
上面的代码我们来分析一下,最上面的if就是排除一下异常情况,最核心的就是synchronized里面的代码。首先将任务添加到队列中,然后根据nextExecutionTime调整队列。
添加任务add(task):
![](http://pics0.baidu.com/feed/2fdda3cc7cd98d10a99a18a00e993a0b7aec9004.jpeg?token=4e006179fd880a8f3966efaddb28078a&s=B281B14C8FE4BD7054D974030000A0C3)
维护最小堆:
![](http://pics5.baidu.com/feed/b812c8fcc3cec3fd87420d01f92e563a869427f5.jpeg?token=7c2fe75eb8a2b2a2d1624eabd5de421e&s=B281B14C9FE48D701C55BC0F0000F0C1)
上面就是Timer中如何执行的定时任务核心,但是还有一个方法,也是执行定时任务的。叫scheduleAtFixedRate
下面我们来分析一下,然后比较和上面的不同。
4、scheduleAtFixedRate方法
这个方法有两个:
(1)scheduleAtFixedRate(task, time, period)
在时间等于或超过time的时候首次执行task,之后每隔period毫秒重复执行一次task 。这个time表示的是例如2019年11月11日上午11点11分11秒。指的是时刻。
(2)scheduleAtFixedRate(task, delay, period)
在delay时间之后,开始首次执行task,之后每隔period毫秒重复执行一次task ,这个delay表示的是延迟时间,比如说三秒后执行。
既然上面都已经有了4个定时器,为什么这里还要再增加几个呢?我们来分析一下他们的区别:
分两种情况: ① 首次计划执行的时间 schedule:如果第一次执行时间被delay了,随后的执行时间按照上一次实际执行完的时间点进行计算 。scheduleAtFixedRate:如果第一次执行时间被delay了,随后的执行时间按上一次开始的时间进行计算,并且为了赶上进度会多次执行任务,因此TimerTask中的执行体需要考虑同步。
②任务执行所需时间 schedule方法:下一次执行时间会不断延后,因此参照的是上一次执行完成的时间点。scheduleAtFixedRate方法:下一次执行时间不会延后,因此存在并发性。我们可以看一下图:
![](http://pics2.baidu.com/feed/622762d0f703918f3e3ed7007e9ba4925beec4bd.jpeg?token=307ecf045258ab76050f9a37d302c48b&s=35947D329B5855C8484DF8C600005031)
5、其他方法
我们已经明白了如何创建Timer和执行定时任务,如果在执行的时候我们突然改变主意,想要取消怎么办呢?这里Timer当然为我们提供了。
(1)cancel:取消此计时器任务。
(2)scheduledExecutionTime():返回此任务最近实际执行的安排执行时间。
6、任务调度
任务调度也就是说我们的线程如何去执行这些任务。其实在TimerThread调用了run来执行,我们看一下源码。
![](http://pics2.baidu.com/feed/503d269759ee3d6df7b953296cb0ef274e4ade05.jpeg?token=2de1eb33cfe700a828d6af1645de6b29&s=BA81B14C4AE48F6E42D0F10B00003081)
也就是说其实真正执行任务调度的是mainLoop(),synchronized代码块只是为了确保在执行完之后能够移除这个task。
而这个mainLoop方法的思想很简单,就是拿出任务队列中的第一个任务,如果执行时间还没有到,则继续等待,否则立即执行。源码在这里就不再给出了。
三、Timer缺陷
上面从源码的角度分析了一下Timer,因为用法很简单,主要是源码分析。说了这么多,Timer还是有一定的缺陷的,
1、Timer管理延时任务的缺陷
Timer在执行定时任务时只会创建一个线程,所以如果存在多个任务,且任务时间过长,超过了两个任务的间隔时间,会发生一些缺陷。我们看一个例子:
这个例子中的功能是这样的,第一个任务在1秒钟之后开始执行,第二个任务在2秒钟之后开始执行。
第一步:定义两个TimerTask
![](http://pics0.baidu.com/feed/21a4462309f79052e2bd6043235555cf7acbd55c.jpeg?token=15f9218617ae1bf38b756f074ea0c3fa&s=B281B14C12F4B46F1C59D40F0000E0C1)
还有一个:
![](http://pics3.baidu.com/feed/f7246b600c338744a55351307ea95bfcd62aa017.jpeg?token=fbb16881fd9c506b6394e05d86d0e721&s=3281B14C03F08D684E41300F0000F0C1)
第二步:我们测试一下:
![](http://pics0.baidu.com/feed/cefc1e178a82b901721aece45c2b2b723b12efd9.jpeg?token=3642b7973101ef5a5ced7dbbb5b1ffe2&s=3A81B14CDAEC8C4D1CDC6D03000070C1)
我们在上面的Task1中会发现,任务2不是应该在32秒的时候执行嘛,怎么会在4秒钟之后才执行。究其原因是任务1执行了3秒,但是线程只有一个,所以只能先把任务1执行完才去执行任务2。这就是其缺陷之一。
2、Timer当任务抛出异常时的缺陷
这个缺陷的意思是,其中有一个任务抛出了RuntimeException,那么所有的任务都会停止执行。这个演示起来很简单。
第一步:声明几个定时任务
![](http://pics1.baidu.com/feed/d52a2834349b033b88d7185e3a68b4d6d439bd07.jpeg?token=3d6bdf566d2f7427ce2cfc3ad5f00e6e&s=3281B14E52FEAE695EC19506000030C1)
第二步:测试
![](http://pics7.baidu.com/feed/dcc451da81cb39dbe3ed86d5ffb08b21aa1830ab.jpeg?token=3be112e30deec71ebb183f892b32b184&s=3AC1B14C13F49C6B5EEC058F0200E0C1)
我们来看一下结果:
![](http://pics4.baidu.com/feed/5882b2b7d0a20cf4fe603bbb46afc933adaf990a.jpeg?token=5a99a481a558f5e300f659029069ddc7&s=7AD5AC1A97E04D015A60F2CA020020B1)
正是Timer有很多的缺陷,所以出现了Timer的替代品ScheduledExecutorService,用来解决上面出现的问题。而且也出现了很多优秀的框架。具体的我会在后续文章中介绍。
OK,今天的文章到这,欢迎批评指正。