定时任务之Timer

Timer,和handler的post机制类似。都可以用于之后某个时刻执行任务的实现,Timer恒存在自己的子线程,任务也在该子线程中执行;handler.post出去的任务在什么线程执行取决于其Looper所在线程。

一、简介

1.1 Timer原理简述

Timer是一个用于执行定时任务的类,可以单次执行或按指定时间间隔循环执行(直到主动cancel或线程被杀掉)。Timer中任务处理采用了生产者-消费者模型的设计思想。具体知识点包含以下几点:

1)一个存储任务的任务池TaskQueue,包含一个初始大小为128的TimerTask数组,负责任务的存储(add)、排序(fixUp、fixDown)、取出(getMin)、清理(removeMin、quickRemove)、循环任务处理(rescheduleMin)以及一些其他基本操作。并通过排序保证队头任务的执行一定是最早的。

2)一个作为事件消费者的TimerThread,TimerThread中不断获取当前任务队列的队头任务,执行任务。并根据任务是否需要循环决定是移除任务还是将任务按下一次执行时间重新加入到任务队列中。在TimerThread中不断获取待执行任务时,采用了Object.wait()和Object.notify()的机制。Object.wait()保证了任务队列为空时及时释放资源,而当有新的任务时也通过Object.notify()及时恢复任务的遍历。

3)TimerTask是任务实体,Runnable接口的实现类。内部包含用于线程安全的锁lock、用于标记任务状态的字段state、以及一个供用户实现的任务内容抽象方法run().

4)Timer本身提供了对以上三者操作的封装、实例化和对外暴露运行任务的接口。同时,作为生产者,将用户任务加入到任务队列;对消费者层面,Timer也是和消费者线程唯一绑定的,负责启动消费者线程,并在生产了新的任务后及时通知已经休眠的消费者。提供了多种构造方法和清理接口。

Timer是一个控制中心,对外和用户交互,接收用户要定时执行的任务;对内既是生产者又是消费者,控制 任务的存储、调度、执行的发起者。具体到TimerThread、TimerTask其实是一种委托思想(类似android中事件机制),Timer委托二者一个作为生产者、一个作为消费者。Timer不断将新任务按序交给生产者保存,同时委托消费者线程不断消费生产者队列中待执行的任务,并根据是否循环进行调度。

对于取消操作,消费者线程的根据newTasksMayBeScheduled字段和消费队列是否为空双重保障。而任务本身通过资深的状态字段进行控制。

1.2 Timer和Handler(或view)的post/postDelay

Timer和Handler.post都可以执行延时任务。

二者的相同之处如下:

1)执行的任务均为Runnable的实现类。

2)均可以指定延时时长delay。

3)二者皆通过生产者-消费者模型实现。

二者的不同之处:

1)Timer的任务是在新开的子线程中执行的,所以不可以做更新UI的操作,但可以做耗时任务(对于存在多任务的case,不建议又耗时任务,因为耗时任务会影响后续任务的执行时间);Handler.post方式执行的任务是在Looper对应线程中执行的,能否更新UI及做耗时任务取决于Looper对应线程是否是主线程。

2)Handler.post的方式执行任务只能执行一次(可通过递归调用、自调用的方式实现repeat);Timer封装好了相关接口,并提供了两种不同的循环方式。

3)从代码层面,Looper既是生产者(容器)也是消费者。并采用ThreadLocal机制将自身和线程一一绑定,也就有了任务执行取决于Looper的产生。具体下一篇再分析。

4)handler中提供了handleMessage方法,是另一种消息分发。可以和以上两种机制结合使用,用来切线程。

二、源码分析

2.1 任务池TaskQueue

在其中存在一个初始大小128的array,根据注释可以知道这是一个用做一个平衡二叉树的模型,一个父节点array[n]下挂载的两个子节点为array[2n]和array[2n+1].

2.1.1 任务实体TimerTask

TimerTask是实现了Runnable的抽象类。

lock用于锁实现,为外边获取来进行线程同步;state值用于标记任务状态。nextExecutionTime是任务的下次执行时间,在执行时用该字段和当前时间比较决定是否到了执行的时候。使用地方可以看如下代码:


这段timer中的代码是任务的调用处理,首先从任务队列里边取出队头(即最先被执行的任务),获取任务的锁,判断任务状态是否被置为cancelled,如果是则移除该任务再遍历下一个。如果没有则比较当前时间和任务应执行时间,如果应该执行,则判断是不是循环任务,如果是则将任务按下一次执行时间加入到任务池中。

二叉树的插入和删除逻辑为


在插入任务时,首先判断当前队列是否能装下,不能的话会以queue.length*2的方式扩展。之后调整位置。
在删除队头的时候,把队头指向队尾元素,同时调整队头的位置。
fixup和fixdown用于调整位置。以nextExecutionTime从小到大,二叉树从上向下的方式排列,且二叉树的父节点和两个子节点的下标为n和2n/2n+1.具体算法请看平衡二叉树算法,不展开详解。

2.2 任务的消费者线程TimerThread


以上是消费者线程,在其中当任务池中为空且会再执行时,会wait(),直到队列中有新任务时notify()唤醒,如下:


这是timer主体中外界添加新任务时,如果add的任务正好是要执行的(证明在add该任务之前,任务队列中无可执行任务,也即线程已经wait()),则通过notify唤醒。

这样消费者线程在任务队列有任务时,通过循环不断执行任务,无任务时,若想让线程停止退出则要通过改变线程字段的方式。如下:


在循环中,

1、队列为空且newTasksMayBeScheduled成立,则wait等待

2、1不成立,且队列为空(即newTasksMayBeScheduled不成立),则跳出循环,线程执行完毕跳出。再看run方法中finally中为清理工作。


2.3 timer对外接口


对于用户调用,timer提供了两组方法,schedule和scheduleAtFixedRate,这两组方法又都会调用到sched方法,注意此时的第三个参数,period和-period,正数和负数的差别。最终影响为第三张图。

结论:当period<0时,不论任务执行时间是否延误,下一次都在固定时间执行,参照系为当前时间

当period>0时,任务在下一次时间后一定时间执行,参照系为下一次具体执行时间。

三 使用方法


使用方法很简单,调用对应方法即可。

最后有几点需要注意:

1)task是一定执行在timer子线程中(不论调用线程是否为主线程),所以若需要在任务中执行更新ui的操作,可以通过runonuithread或显示通过handler方式切回主线程。

2)在不使用时一定要及时cancel清理,释放资源。

3)当timer中有多任务时,因为后边任务会依赖前边任务执行完,尤其是如果有耗时任务,会发生定时不准确的现象。

4)当存在多任务时,若其中某个因异常而终止,则会退出所有任务的执行(消费者线程被异常终止了)


头一次写东西,用来记录自己的所学所获,文笔不好,理解可能也有所片面,希望看到的朋友不吝赐教,共同进步。

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

推荐阅读更多精彩内容

  • 美图欣赏 Java、Android知识点汇集 Java集合类 ** Java集合相关的博客** java面试相关 ...
    ElvenShi阅读 1,743评论 0 2
  • Android Handler机制系列文章整体内容如下: Android Handler机制1之ThreadAnd...
    隔壁老李头阅读 8,188评论 8 57
  • 第5章 多线程编程 5.1 线程基础 5.1.1 如何创建线程 在java要创建线程,一般有==两种方式==:1)...
    AndroidMaster阅读 1,793评论 0 11
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,724评论 2 59
  • 11.05 为什么我会突然想哭 有时候我觉得幸福会蜷缩 因为有些声响太刺耳、尖锐 说不出那是一种什么感觉 恍若,在...
    佐恬阅读 211评论 0 0