Android平台中有许多的定时器比如说CountDownTimer和Timer,定时器就像平时使用的定时闹钟一样,在固定的时候就会执行某种任务,也在可以反复的做某种事情。其中Timer是Java老牌的定时器平时我们估计用的特别多,但是对于其内部的实现却知之甚少的。
基本使用
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
//注意这里的代码是在子线程中执行的,如果是在Android里面的话,这里不能做UI操作
System.out.println("current thread..." + Thread.currentThread().getName());
}
};
mTimer.schedule(mTimerTask, 10000, 10000);
代码分析
TimerTask其实是一个非常简单的抽象类直接继承了Runnable接口,如果直接继承该类并且实现该类的run() 方法就可以了,里面包含这种对应的状态。
public abstract class TimerTask implements Runnable {
final Object lock = new Object();
int state = VIRGIN;
//表示尚未计划此任务(也表示初始状态)
static final int VIRGIN = 0;
//表示正在执行任务状态
static final int SCHEDULED = 1;
//表示执行完成状态
static final int EXECUTED = 2;
//取消状态
static final int CANCELLED = 3;
//下次执行任务的时间
long nextExecutionTime;
//执行时间间隔
long period = 0;
//我们需要实现该方法,执行的任务的代码在该方法中实现
public abstract void run();
//取消任务,从这里我们可以很清楚知道取消任务就是修改状态
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
}
相比TimerTask来讲Timer才是真正的核心,在创建Timer对象的同时也创建一个TimerThread对象,该类集成Thread,本质上就是开启了一个线程。
public class Timer {
//创建一个任务队列
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(boolean isDaemon) {
this("Timer-" + serialNumber(), isDaemon);
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
public Timer(String name, boolean isDaemon) {
thread.setName(name);
thread.setDaemon(isDaemon);
thread.start();
}
}
上面我们知道了在创建Timer对象时其开启了一个线程TimerThread。该线程是一个不断循环的。
class TimerThread extends Thread {
boolean newTasksMayBeScheduled = true;
private TaskQueue queue;
TimerThread(TaskQueue queue) {
this.queue = queue;
}
public void run() {
try {
mainLoop();
} finally {
synchronized(queue) {
//同时将状态置为false
newTasksMayBeScheduled = false;
//清空队列中所有的任务
queue.clear();
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
//如果任务队列为空并且该标志位 true的话,则该线程一直进行等待中,直到队列中有任务进来的时候执行 queue.notify才会解除阻塞
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
//如果队列中的内容为空的话直接跳出循环,外部调用者可能取消了Timer
if (queue.isEmpty())
break;
long currentTime, executionTime;
//获取队列中最近执行时间最小的任务(也就是最近需要执行的任务)
task = queue.getMin();
synchronized(task.lock) {
//如果该任务的状态是取消状态的话,那从队列中移除这个任务,然后继续执行循环队列操作
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue;
}
//获取当前系统时间
currentTime = System.currentTimeMillis();
//获取下一个目标要执行的时间
executionTime = task.nextExecutionTime;
//如果下一个目标要执行的时间大于等于等于时间了,表示要执行任务了
if (taskFired = (executionTime<=currentTime)) {
//如果task的时间间隔为0,表示只执行一次该任务
if (task.period == 0) {
//将任务状态改为已执行状态,同时从队列中删除该任务
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
//将任务重新跟队列中的任务进行排列,要始终保证第一个task的时间是最小的
queue.rescheduleMin(task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
//这里表示最近要执行的任务时间没有到,那么再让当前的线程阻塞一段时间
if (!taskFired)
queue.wait(executionTime - currentTime);
}
//表示要执行的任务时间已经到了,那么直接调用任务的run() 执行代码
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}
}
首先开启一个不断循环的线程如果队列中不存在任务则阻塞当前的线程,直到队列中添加任务以后唤醒线程。然后获取队列中执行时间最小的任务(这里用到了一个非常重要的数据结构——最小堆,有兴趣的童鞋可以自己百度查查,对应的还有**最大堆**),如果该任务的状态是取消的话则从队列中移除掉再从队列中重新获取。最后判断当前的时间是否大于等于任务的执行的时间,如果任务的执行时间还未到则当前线程再阻塞一段时间,同时我们还要将该任务重新扔到任务队列中重新排序,我们必须保证队列中的第一个任务的执行时间是最小的。
public class TaskQueue {
//创建一个数组为128的数组存放需要执行的任务,如果超过了128的话则扩容为原来的两倍
private TimerTask[] queue = new TimerTask[128];
//用于统计队列中任务的个数
private int size = 0;
//返回队列中任务的个数
int size() {
return size;
}
//依次遍历数组中的任务,并且置为null,有利于内存回收,注意这里的下标是从1开始计算的,不是从0
void clear() {
for (int i=1; i<=size; i++)
queue[i] = null;
size = 0;
}
//这里添加一个新的元素使用的是最小堆的操作,这里不详细说明了。
void add(TimerTask task) {
//如果数组已经存满任务,那么扩容一个新的数组为之前的两倍
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
queue[++size] = task;
fixUp(size);
}
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
}
通过上面的代码分析中我们可以得出任务队列的实现是基于数组的最小堆来实现的,每次取出的第一个任务的执行时间保证是最小的。具体的细节这里就不过多的细说最大堆,最小堆的实现原理大家可以看看网上关于PriorityQueue代码分析与实现。
发布任务
当我们创建好Timer并且启动了循环线程以后,这个时候我们就需要发布任务。发布任务主要有以下几个方法。
- schedule(TimerTask task, Date time)
表示第一次执行任务的时间,时间间隔为0,也表示该任务只执行一次就结束了
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
- schedule(TimerTask task, Date firstTime, long period)
firstTime 表示第一次执行的时间,period表示执行任务的时间间隔也就是多久时间执行一次
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
- schedule(TimerTask task, long delay)
延迟 delay时间执行任务,也就是在当前的时间+delay执行任务(该方法只执行一次任务)
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
- sched(TimerTask task, long time, long period)
上面所有的执行任务的函数最后都是调用的该方法,task表示要执行的任务,time表示要执行任务的时间,period表示任务执行的间隔时间。
private void sched(TimerTask task, long time, long period) {
//如果时间间隔大于 long最大值的一般的话,需要对该数值 /2
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
//首先判断轮训线程是否取消,如果取消状态直接抛出异常
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
synchronized(task.lock) {
//判断新执行的任务状态如果不是初始化状态话,直接抛出异常
if (task.state != TimerTask.VIRGIN)
throw new IllegalStateException("Task already scheduled or cancelled");
//赋值下次执行任务的时间
task.nextExecutionTime = time;
task.period = period;
//将任务状态修改为发布状态
task.state = TimerTask.SCHEDULED;
}
//将任务添加到最小堆队列中,注意:这里在添加到队列里面要保证第一个元素始终是最小的
queue.add(task);
//如果task就是队列中最小的任务话,则直接唤醒轮训线程执行任务(也就是唤醒TimerThread线程)
if (queue.getMin() == task)
queue.notify();
}
}
从上面的代码中可以清楚的明白发布任务非常简单的,就是往任务队列中添加任务然后判断条件是否需要唤醒轮训线程去执行任务。其核心代码是在 TimerThread 轮训中以及使用最小堆实现的队列保证每次取出来的第一个任务的执行时间是最小的。
缺陷
Timer通过一个寻轮线程循环的从队列中获取需要执行的任务,如果任务的执行时间未到则进行等待(通过Object类的 wait 方法实现阻塞等待)一段时间再自动唤醒执行任务。但是细心的我们发现这个是单线程执行的如果有多个任务需要执行的话会不会应付不过来呢?这个好比平时生活中如果让一个人去做多件事情的话,要是所有的事情所耗费的时间很短的话,那么就不会出现延迟问题,要是其中一件或者是某件事情非常耗时间的话那么则会影响到后面事情的时间。其实这个现象一样跟Timer出现的问题也是一样的道理,如果某个任务非常耗时间,而且任务队列中的任务又比较多的话,那 TimerThread 是忙不过来的,这样子就会导致后面的任务出现延迟执行的问题,进而会影响所有的定时任务的准确执行时间。那么有人就会想要可以一个TimerTask对应一个Timer不就行了吗?但是我们要清楚的明白计算机的系统资源是有限的,如果我们一个任务就去单独的开一个轮训线程执行的话,其实是有一点浪费系统的资源的,完全没有必要的,如果不需要定时任务了话,我们还需要去销毁线程释放资源的,如果是这样子的反复操作的话,不利于我们程序的流畅性。于是乎 ScheduledThreadPoolExecutor 就被我们聪明的开发者天马行空的想出来了。
总结
综上所述Timer其实就是一个轮训线程,不断去从一个任务队列中获取任务,如果队列中没有任务则当前线程进入等待状态,直到队列中增加任务后会自动唤醒,不过这里的队列的实现用的是最小堆算法来实现的。这个里面主要还是两个知识点:最小堆和线程等待和唤醒了(Object的wait和notify其实是老生常谈的话题,没有啥可说的。)