Timer时间控制器的源码解析

在介绍之前,还是经典的几个问题:

1、Timer是什么?能干什么?

2、Timer的使用案例?

3、Timer的原理?

4、Timer教其他同类工具的优缺点?

    1、Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。

    2、Timer的使用,这个先来一个简单的demo。

基本的Demo

结果如下:

demo运行结果

    3、要介绍原理,得先从源码入手,看下Timer的方法如下:

Timer内部的方法

    其中我们要重点介绍schedule方法和scheduleAtFixedRate方法,这个是Timer能实现定时任务的核心。

    还有我们要介绍Timer类的两个内部类:

    1、TaskQueue,TaskQueue是一个队列,看下里面的内容。

TaskQueue的方法和成员变量

    其中存储的是TimerTask类,上面demo里面的PrintTask就是TimerTask的之类,最终也是会进入这个队列里面的。

    看下add(TimerTask task)方法代码。

add(TimerTask task)方法代码

    如果长度超过队列的长度,就把队列扩展,生成一个新队列赋值给queue变量。

fixUp(size)是关键代码,看下到底做了什么?

fixUp(size)代码逻辑,对后面有参考作用

    其中nextExecutionTime为TimerTask的字段,表示下一次执行的时间戳。

    所以由上面代码可以知道,目的是为了进行排序,先不管是什么排序(好像是二元选择,由于今天说的不是排序,大家可以去看下),目的是把刚刚添加的这个时间任务根据他的nextExecutionTime放到合适的位置。按照下标的顺序从小到大排序。

    2、接下来介绍另外一个内部类TimerThread,看类名就可以知道是个线程类。

TimerThread内部结构

看下类的内部结构:

一个构造方法,参数是TaskQueue(TimerThread(TaskQueue))

一个newTasksMayBeScheduled的布尔类型成员变量,用来标识是否有可能有新任务被安排。

一个私有的方法mainLoop()这个是方法是核心。

一个run方法,每个Thread都会复写的。

先看下run方法的代码:

线程的run方法

由此可知,其中调用了mainLoop()方法,finally块中表名,线程停止是否,会清掉队列,设置newTasksMayBeScheduled为false;

接下来看mainLoop方法:

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,所以设计者就用了生产消费者模式,使用Objectwait/notify这类特性,进行及时通知。让线程及时被唤醒,这个线程起来跑起任务,就会非常及时执行任务。(正常情况下,如果这个时候机器有其他大型运算在进行,可能线程就会有稍微一点延迟唤醒,这个基本上可以忽略不计)

    Object.wait(long timeout)与Object.wait()方法不一样,虽然都是可以让对象挂起,但是wait(long timeout)超时会自动唤醒,而wait()则只能等待被notify(),notifyAll()方法唤醒,否则会一直沉睡下去。

现在原理已经知道了,但还是个疑问就是第3点,如果线程进入wait了(消费者消费等待),谁来唤醒他(生产者生产消息)?

看以下代码:

sched方法

这个方法是所有要添加到队列里面的任务的最终方法,他的上层代码会抽成不重复执行,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.

get方法

以上我们分析了任务的根本设计,就是任务如何做到定时启动的。

以上的设计,其中的基本流程图如下:


流程图

现在我们要分析其中未分析到周期执行任务。

周期执行任务主要有两种方法:

1、


schedule方法

2、


scheduleAtFixedRate方法

看着两个方法的代码,除了方法名和参数校验外,基本上差别不大,如果去掉忽略掉队period的处理,完全就是同一个方法。

我们知道这两个方法都有同一个特性,就是可以对任务进行周期性执行,上Demo。

demo

在上面的demo上做了些修改,结果如下:

以上demo的执行结果

可以看到,这两个方法的允许结果没有差别。

其实这两个方法是有区别的,可以看出来scheduleAtFixedRate的方法名注释就是在第一次执行时间早于当前时间时,她会进行补充,这个可以通过实验说明,我们现在把第一次执行的时间在当前时间之前30S执行,10S执行一次。

追赶性Demo

执行结果如下:

执行结果

从结果可以看出来,D任务在10:47:34的时候,除了和C一样在第一次执行的时候都会执行之外,她有执行了3次,刚好补充上“缺失”30S时间。

所以scheduleAtFixedRate方法具有“补充性”,一种翻译叫做“追赶性”。

接下来还是解读一下源码吧:

从上文中我们知道,period参数在校验过去后,直接赋值给Task的period

由于上面已经有了mainLoop的全部代码,我这边就截取最关键的代码进行说明:

mainLoop关键代码

前面介绍了,541行的if(period==0)表示非周期性执行,则从队列中去掉这个任务,并且设置任务的状态为执行完毕。

else是周期性执行,最关键的代码是两部分:

1)rescheduleMin,表示的是设置一个新时间给当前队列中head任务queue[1],其实就是当前的任务了,后面会进行finxDown(1)的排序。所以当前的任务就变成一个新任务加入到队列中。

rescheduleMin方法

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次,才能追赶上CcurrentTime)所以这边的追赶性就是这个原理。

上面还有个问题,还没说明,就是为何调用run,这样不就并行了么,是否会导致如果上面的任务如果执行时间太长,影响下面任务的执行。

这个是事实,大家可以做实验,这边明显就是串行执行的,虽然TimerTask是一个线程类,但是最终没有以线程的方式启动它,这就导致他的时效性有时候难以保证,还有就是如果其中某个任务异常了,这个时候异常是直接抛到启动它的主线程里面,导致所有任务都停止了,这个可以从源码中可以看出,while里面也没有针对异常进行处理。

这个设计者最初一定是有原因的,看业务来做吧,如果有可能出现时间过长的任务需要处理,且后面的任务对实时性要求教高,就建议用别的工具。

优点:单线程,省线程资源,且使用方便。

缺点:各个任务之间可能会造成互相影响。Timer当任务抛出异常时的缺陷,如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行。

以下是简单流程图:

接下来简单介绍其他的几种任务调度器:

ScheduledThreadPoolExecutor

这个也是jdk带的一个任务调度器。

ScheduledThreadPoolExecutor部分代码

是从jdk1.5开始进入并发工具包,作者是Doug Lea大神。

这边改为一个任务一个线程。

优点:修复Timer上面的各个任务之间互相影响的问题。

缺点:耗费太多线程了,很容易造成OOM,而且功能较少,上面的Timer也一样。

以上两个jdk自带的工具类,都有一些缺陷,Timer和ScheduledExecutor都仅能提供基于开始时间与重复间隔的任务调度,不能胜任更加复杂的调度需求。比如,设置每星期二的16:38:10执行任务。该功能使用Timer和ScheduledExecutor都不能直接实现,但我们可以借助Calendar间接实现该功能。

开源工具包Quartz

Quartz就能解决以上痛点,看下介绍:

Quartz是个开源的作业调度框架,为在Java应用程序中进行作业调度提供了简单却强大的机制。Quartz框架包含了调度器监听、作业和触发器监听。你可以配置作业和触发器监听为全局监听或者是特定于作业和触发器的监听。Quartz允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了Quartz的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业。并且还能和Spring配置整合使用。

缺点还是线程问题过多咯。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342