在介绍之前,还是经典的几个问题:
1、Timer是什么?能干什么?
2、Timer的使用案例?
3、Timer的原理?
4、Timer教其他同类工具的优缺点?
1、Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。
2、Timer的使用,这个先来一个简单的demo。
结果如下:
3、要介绍原理,得先从源码入手,看下Timer的方法如下:
其中我们要重点介绍schedule方法和scheduleAtFixedRate方法,这个是Timer能实现定时任务的核心。
还有我们要介绍Timer类的两个内部类:
1、TaskQueue,TaskQueue是一个队列,看下里面的内容。
其中存储的是TimerTask类,上面demo里面的PrintTask就是TimerTask的之类,最终也是会进入这个队列里面的。
看下add(TimerTask task)方法代码。
如果长度超过队列的长度,就把队列扩展,生成一个新队列赋值给queue变量。
fixUp(size)是关键代码,看下到底做了什么?
其中nextExecutionTime为TimerTask的字段,表示下一次执行的时间戳。
所以由上面代码可以知道,目的是为了进行排序,先不管是什么排序(好像是二元选择,由于今天说的不是排序,大家可以去看下),目的是把刚刚添加的这个时间任务根据他的nextExecutionTime放到合适的位置。按照下标的顺序从小到大排序。
2、接下来介绍另外一个内部类TimerThread,看类名就可以知道是个线程类。
看下类的内部结构:
一个构造方法,参数是TaskQueue(TimerThread(TaskQueue))
一个newTasksMayBeScheduled的布尔类型成员变量,用来标识是否有可能有新任务被安排。
一个私有的方法mainLoop()这个是方法是核心。
一个run方法,每个Thread都会复写的。
先看下run方法的代码:
由此可知,其中调用了mainLoop()方法,finally块中表名,线程停止是否,会清掉队列,设置newTasksMayBeScheduled为false;
接下来看mainLoop方法:
以上代码是Timer实现的关键,注意前提是queue成员函数已经是按照执行时间排好序的(上面已经在fixUp中介绍过了),先不考虑周期等情况下(period=0未非周期性执行),我们再解读一下源码:
1.518行执行的mainLoop函数,顾名思义,就是主要的循环函数,里面有个死循环。
2.523行有个锁,锁住的对象是任务队列queue。
3.525行代表是队列为空且有可能有新的任务被安排时,会执行queue.wait()函数,线程进入wait状态(让出对临界资源的占用权),等待被notify。(等待生产者生产消息)。
4.532行把当前队列中最小的任务赋值给task变量。
5.540行是未taskFired赋值,且判断是否要执行,taskFired是任务的下一次执行时间和当前时间的比较,如果<=当前时间则为true,反之false。
6.541行代表的是如果是非周期性执行的话,这删除当前队列中最小的那个。
从532行可以看到就是当前进行比较的task,而且在removeMin()方法中也会进行一次排序,这边就不再介绍。
7.551行的代码是说如果当前的任务执行实际未到taskFired==false,就会执行queue.wait(timeout)函数,其中的timeout就是超时时间,到了超时时间代码会自动唤醒,重新获取锁。
8.554行的代码可以看到,如果任务已经可以执行了,就会指教调用task.run(),这边有个疑问?就是既然是个task,且这个TimerTaskimplementsRunnable ,应该是按照线程的方式启动,应该newThread(task).start。这边为什么只是单纯的调用run方法而已,会导致什么问题,后面会介绍。
9.此处代码的解释是:用一个进程里面的死循环来监控队列(已经排完序了),但是又不能一直轮询下去,这样很耗CPU,所以设计者就用了生产消费者模式,使用Object的wait/notify这类特性,进行及时通知。让线程及时被唤醒,这个线程起来跑起任务,就会非常及时执行任务。(正常情况下,如果这个时候机器有其他大型运算在进行,可能线程就会有稍微一点延迟唤醒,这个基本上可以忽略不计)
Object.wait(long timeout)与Object.wait()方法不一样,虽然都是可以让对象挂起,但是wait(long timeout)超时会自动唤醒,而wait()则只能等待被notify(),notifyAll()方法唤醒,否则会一直沉睡下去。
现在原理已经知道了,但还是个疑问就是第3点,如果线程进入wait了(消费者消费等待),谁来唤醒他(生产者生产消息)?
看以下代码:
这个方法是所有要添加到队列里面的任务的最终方法,他的上层代码会抽成不重复执行,period=0,时间为Date和delay ,最终time=Date.getTime()或者System.currentTimeMillis()+delay等。
咱们此处还是只针对非周期性的任务进行分析。
Period=0。
以上的代码可以看到有两个synchronized锁,synchronized(queue):395~411行,synchronized(task.lock):399~406行。
以上的代码主要做了几件事如下:
1.line395,一次只允许一个线程操作queue对象.
2.Line403~line405可知,这里面把一些所需要的重要属性都赋值给task
3.line408添加任务进入队列中,这个是最重要的,在上文中写道这个方法会对task进行排序。
4.line409表示当queue.getMin()==task,内部就是添加到队列之后,queue[1]==task则执行queue.notify(),唤醒mainLoop方法里面queue.wait方法,这个时候就可以进行执行。
至于这边有个奇怪的地方,就是queue的最小是任务是queue[1],其实是TaskQueue这个内部类的设计(这个地方需要讨论一下为什么要这么设计),她直接跳过task[0],从以下get(i)的注释中可以看出来,队列的头在数组中的下标是1.
以上我们分析了任务的根本设计,就是任务如何做到定时启动的。
以上的设计,其中的基本流程图如下:
现在我们要分析其中未分析到周期执行任务。
周期执行任务主要有两种方法:
1、
2、
看着两个方法的代码,除了方法名和参数校验外,基本上差别不大,如果去掉忽略掉队period的处理,完全就是同一个方法。
我们知道这两个方法都有同一个特性,就是可以对任务进行周期性执行,上Demo。
在上面的demo上做了些修改,结果如下:
可以看到,这两个方法的允许结果没有差别。
其实这两个方法是有区别的,可以看出来scheduleAtFixedRate的方法名注释就是在第一次执行时间早于当前时间时,她会进行补充,这个可以通过实验说明,我们现在把第一次执行的时间在当前时间之前30S执行,10S执行一次。
执行结果如下:
从结果可以看出来,D任务在10:47:34的时候,除了和C一样在第一次执行的时候都会执行之外,她有执行了3次,刚好补充上“缺失”30S时间。
所以scheduleAtFixedRate方法具有“补充性”,一种翻译叫做“追赶性”。
接下来还是解读一下源码吧:
从上文中我们知道,period参数在校验过去后,直接赋值给Task的period。
由于上面已经有了mainLoop的全部代码,我这边就截取最关键的代码进行说明:
前面介绍了,541行的if(period==0)表示非周期性执行,则从队列中去掉这个任务,并且设置任务的状态为执行完毕。
else是周期性执行,最关键的代码是两部分:
1)rescheduleMin,表示的是设置一个新时间给当前队列中head任务queue[1],其实就是当前的任务了,后面会进行finxDown(1)的排序。所以当前的任务就变成一个新任务加入到队列中。
1)三目表达式:task.period<0? currentTime- task.period
: executionTime + task.period
小于0非补充性的重复任务,这新时间为当前时间-task.period,由于前文可知非补充性的任务period为设置值得负值,所以假设我们要10秒钟跑一次,这边相当于currentTime+10S解决。
大于0表示补充性的重复任务,还是假设10S跑一次,这边是executionTime+10S所以也正常。
最后怎么样解释他的补充性,用上面demo里面的任务来理解吧:
现在是Mon Dec 11 10:47:34 CST 2017
[C]的打印时间为:Mon Dec 11 10:47:34 CST 2017
[D]的打印时间为:Mon Dec 11 10:47:34 CST 2017
[D]的打印时间为:Mon Dec 11 10:47:34 CST 2017
[D]的打印时间为:Mon Dec 11 10:47:34 CST 2017
[D]的打印时间为:Mon Dec 11 10:47:34 CST 2017
[D]的打印时间为:Mon Dec 11 10:47:44 CST 2017
[C]的打印时间为:Mon Dec 11 10:47:44 CST 2017
[D]的打印时间为:Mon Dec 11 10:47:54 CST 2017
[C]的打印时间为:Mon Dec 11 10:47:54 CST 2017
[D]的打印时间为:Mon Dec 11 10:48:04 CST 2017
[C]的打印时间为:Mon Dec 11 10:48:04 CST 2017
[D]的打印时间为:Mon Dec 11 10:48:14 CST 2017
[C]的打印时间为:Mon Dec 11 10:48:14 CST 2017
粗体字体为补充性的任务。
去除年月日等等信息:(举例分析)
currentTime=10:47:34
executionTime=10:47:34-30*1000=10:47:14
对于C来说:的newTime=currentTime+ 10*1000=10:47:44
对于D来说:的newTime= executionTime + 10*1000=10:47:24(如此重复3次,才能追赶上C的currentTime),所以这边的追赶性就是这个原理。
上面还有个问题,还没说明,就是为何调用run,这样不就并行了么,是否会导致如果上面的任务如果执行时间太长,影响下面任务的执行。
这个是事实,大家可以做实验,这边明显就是串行执行的,虽然TimerTask是一个线程类,但是最终没有以线程的方式启动它,这就导致他的时效性有时候难以保证,还有就是如果其中某个任务异常了,这个时候异常是直接抛到启动它的主线程里面,导致所有任务都停止了,这个可以从源码中可以看出,while里面也没有针对异常进行处理。
这个设计者最初一定是有原因的,看业务来做吧,如果有可能出现时间过长的任务需要处理,且后面的任务对实时性要求教高,就建议用别的工具。
优点:单线程,省线程资源,且使用方便。
缺点:各个任务之间可能会造成互相影响。Timer当任务抛出异常时的缺陷,如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行。
以下是简单流程图:
接下来简单介绍其他的几种任务调度器:
ScheduledThreadPoolExecutor
这个也是jdk带的一个任务调度器。
是从jdk1.5开始进入并发工具包,作者是Doug Lea大神。
这边改为一个任务一个线程。
优点:修复Timer上面的各个任务之间互相影响的问题。
缺点:耗费太多线程了,很容易造成OOM,而且功能较少,上面的Timer也一样。
以上两个jdk自带的工具类,都有一些缺陷,Timer和ScheduledExecutor都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的调度需求。比如,设置每星期二的16:38:10执行任务。该功能使用Timer和ScheduledExecutor都不能直接实现,但我们可以借助Calendar间接实现该功能。
开源工具包Quartz
Quartz就能解决以上痛点,看下介绍:
Quartz是个开源的作业调度框架,为在Java应用程序中进行作业调度提供了简单却强大的机制。Quartz框架包含了调度器监听、作业和触发器监听。你可以配置作业和触发器监听为全局监听或者是特定于作业和触发器的监听。Quartz允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了Quartz的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业。并且还能和Spring配置整合使用。
缺点还是线程问题过多咯。