0. 背景
在Java中,很常见的一个定时器的实现就是 Timer
类,用来实现定时、延迟执行、周期性执行任务的功能。
Timer 是定义在 java.util
中的一个工具类,提供简单的实现定时器
的功能。和它配合使用的,是 TimerTask
类,这是对一个可以被调度的任务的封装。使用起来非常简单,如下示例:
|
|
以上,就是一个简单的使用Timer 的示例,下文将会分析Timer的源码实现。
1. 概述
在Timer 机制中,涉及到的关键类如下:
- Timer: 主要的调用的,提供对外的API;
- TimerTask: 是一个抽象类,定义一个任务,继承自Runnable
- TimerThread: 继承自
Thread
,是一个自定义的线程类; - TaskQueue: 一个任务队列,包含有当前Timer的所有任务,内部使用二叉堆来实现。
以上几个关键类的引用关系如下:
graph TD A(Timer) -->|包含1个| B(TimerThread) A -->|包含1个| C(TimerQueue) B -->|持有引用| C C -->|包含多个| D(TimerTask) B -->|实现一个| E(线程 Thread)
简要描述的话,是:1个
TimerThread —> 实现1个
线程1个
Timer对象 —> 持有1个
TimerThread 对象1个
Timer对象 —> 持有1个
TimerQueue 对象1个
TimerQueue 对象 —> 持有 n个
TimerTask 对象
2. 源码分析
2.1 Timer类的源码分析
源码分析的话,我们最好是按照Timer 的使用流程来分析。
首先,是Timer 的创建:
|
|
那么,或许大家会有一个疑问,thread
成员的初始化呢?这个时候,在代码里面找,就能发现:
可以看到 thread
和 queue
两个成员都是在声明的时候直接初始化的,并且有意思的是,两个成员都是 final
类型的,这也就意味着这两个成员一旦创建就不会再改了,等于说把 thread
、queue
和 Timer 对象
这三者的生命周期强行绑定在一起了,大家一起创建,并且一经创建将会无法改变。
然后,创建了Timer 后,与之相关的队列也已经创建成功,而且相关联的线程也启动了,就可以进行任务的调度了,我们看下它的任务调度方法:
|
|
多个重载的调度方法在经过一些一些列的状态判断、参数设置、以及把delay
时间转换成实际的执行时间
等之后,
最终完成该功能的是 sched
方法,详情见注释部分:
这里涉及到一个需要留意的点,是在调用schedule
方法的时候,会根据TimerTask 的类型来进行不同的计算,进而给TimerTask设置不同的 period 参数,TimerTask 的类型有以下几种:
- 非周期性任务;对应
TimerTask.period
值为0; - 周期性任务,但是没有delay值,即立即执行;对应
TimerTask.period
值为正数; - 周期性任务,同时包含有 delay值;对应
TimerTask.period
值为负数;
在schedule 方法中,会
|
|
2.2 TimerTask 的源码分析
接下来,本文将会分析 TimerTask
的源码。相对于Timer 来说,它的源码其实很简单,TimerTask 是实现了Runnable
接口,同时也是一个抽象类,它并没有对 Runnable 的 run()
方法提供实现,而是需要子类来实现。
它对外提供了以下几个功能:
- 包含有一段可以执行的代码(实现的Runnable 接口的run方法)
- 包含状态的定义。它有一个固定的状态:VIRGIN(新创建)、SCHEDULED(被调度进某一个 timer 的队列中了,但是还没有执行到)、EXECUTED(以及执行过了)、CANCELLED(任务被取消了)。
- 包含有取消的方法。
- 包含有获取下一次执行时间的方法。
相关的源码如下:
|
|
2.3 TimerThread 的源码分析
TimerThread
首先是一个 Thread 的子类,而且我们知道,在Java中,一个Thread 的对象就是代表了一个JVM虚拟机线程。那么,这个 TimerThread 其实也就是一个线程。
对于一个线程来说,那么它的关键就是它的 run()
方法,在调用线程的 start()
方法启动线程之后,接下来就会执行线程的 run()
方法,我们看下 TimerThread 的run()
方法:
从以上可以明确得看出,TimerThread
里的实现是调用 mainLoop()
启动了一个死循环,这个死循环内部的工作就是这个线程的具体工作了,一旦线程的死循环执行完毕,线程的 run 方法就执行完了,线程紧接着就退出了。熟悉Android的朋友可能已经觉得这里的实现非常眼熟了,没错,这里的实现和Android平台的 Handler + HandlerThread + Looper
的机制非常相像,可以认为Android平台最初研发这套机制的时候,就是参考的Timer 的机制,然后在上面做了些升级和适合Android平台的一些改动。
下面是 mainLoop() 方法:
TimerThread 中除了上面的主要逻辑之外,还有一些需要关注的地方,那就是它持有一个 TimerQueue 的对象,这个对象是在创建的时候外部传进来的,也是和当前的Timer 关联的TimerQueue:
|
|
2.4 TimerQueue 的源码分析
TimerQueue
的逻辑上是一个队列,所有它包含有一个队列常见的那些方法,如 size()、add()、clear()等方法。下面我们找一些重要的方法进行分析:
首先,在上文的分析中,我们以及见过TimeQueue 的 getMin()
方法了,这个方法是获取当前的队列里面,接下来应该被执行的TimerTask,也就是说,是执行时间点
数值最小的那一个,那么我们就先看下它的源码:
What??? 就这吗?为啥这么简单?为啥就返回 queue[1]
就对了?
你是不是也有这样的疑问,那么带着疑问往下看吧。
接下来,是添加一个TimerTask 到队列中:
从上文看出,add 方法本身也没什么奇特的,就是很简单地把新的 TimerTask 放在了数据的最新的位置,只是里面调用了一下另一个方法 fixUp() ,好,那么我们接着分析这个方法:
|
|
看了上面对 fixUp()
的分析,是不是仍然一脸懵?或许也有些熟悉算法的朋友已经觉察出些什么了,那么这个地方的逻辑是什么呢?
有了右移一位
、[1, size]的区间
等蛛丝马迹,我想聪明的你已经猜出来了,这个数组queue
里面,是存放了一个完全二叉树
。
我们回想一下用数组实现完全二叉树的算法题(可以参考下:数组实现二叉树)?
在发现 queue 数组是一个二叉树之后,再去理解上面的 fixUp() 方法其实就很简单了,里面的过程是这样的:
- 从二叉树的最后一个叶子结点开始循环;
- 获取这个叶子结点的父结点(完全二叉树中对应的父结点的索引是:子结点位运算右移一位得到的)
- 判断父结点和子结点中对应的 TimerTask 的 nextExecutionTime 的大小,如果父比子的小,则停止循环;如果父比子的大,则交互负责结点;
- 重复以上循环,直到遍历到根结点;
通过以上分析,能发现在每一次新增一个结点后,使用 fixUp(),方法直接对整个二叉树进行了重排序,使得 TimerTask 的nextExecutionTime
值最小的结点,永远被放置在了二叉树的根结点上,也即是queue[1]
。这也就搞明白了为什么 getMin 的实现,是直接获取的 queue[1]
。
同样的道理,在每一次执行 Timer.purge()
方法,清理了TimerQueue中已经取消的Task之后,会执行另一个 fixDown()
方法,它的逻辑正好是和 fixUp()
相反的,它是从根结点开始遍历的,然后到达每一个叶子结点以整理这个二叉树,这里就不再赘述。
回过头来,我们再看下TimerQueue中的实现,会发现它其实是一个二叉堆,二叉堆是一个带有权重的二叉树,详情可以参考:二叉堆。这里不再多说。
3. 总结
通过以上的分析,总的来说,就是每一个 Timer对象中,包含有一个线程(TaskThread)和一个队列(TaskQueue)。TaskQueue 的实现是一个二叉堆(Binary Heap)的结构,二叉堆的每一个节点,就是 TimerTask 的对象。