定时任务之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)当存在多任务时,若其中某个因异常而终止,则会退出所有任务的执行(消费者线程被异常终止了)


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

你可能感兴趣的:(定时任务之Timer)