Timer定时器
通过一个后台线程(成员变量thread)来执行任务调度,每个Timer对象对应的是一个单一的后台线程,用于执行所有的定时器的任务,所以,定时器的任务应该很快完成,如果某个任务执行花费过多时间,它会长时间占用该线程,导致其他任务延迟执行以及扎堆执行。
这个类是线程安全的,多个线程可以共享给一个Timer对象,而不需要外部进行同步。
Timer
1. 成员变量
任务队列,参考TaskQueue
private final TaskQueue queue = new TaskQueue();
执行任务调度的线程,参考TimerThread
private final TimerThread thread = new TimerThread(queue);
这个对象可以理解为线程终结者。这个引用只是纯定义在Timer中,没有再次使用,目的就是在Timer回收之前,优先执行这个引用复写的finalize方法。方法的内容是将变量 newTasksMayBeScheduled (参考TimerThread)设定为false,同时唤醒timerthread线程。
private final Object threadReaper = new Object() {
protected void finalize() throws Throwable {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.notify(); // In case queue is empty.
}
}
};
2. 构造方法
所有构造函数都会启动计时器内部线程thread,有4个构造方法,比较简单没什么说的
public Timer(String name) {
thread.setName(name);
thread.start();
}
3. 核心方法
3.1 任务调度
6个schedule方法其实也没什么好说的,重点在于内部私有方法sched
1. // 延迟 delay 毫秒后执行 task 任务
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
2. // 指定时间点 time 执行 task 任务
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
3. // 延迟 delay 毫秒后,以间隔 period 为一个周期循环执行 task 任务
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, -period);
}
4. // 指定时间点 time 开始执行 task 任务,并且以 period 为周期循环执行
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
5. // 对比3的区别在于AtFixedRate(以固定频率执行),在sched方法中传参period正值,而3传的是负
// 值,正负值的区别涉及到TimerTask,参考下文
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, period);
}
6. // 对比4的区别参考5和3
public void scheduleAtFixedRate(TimerTask task, Date firstTime,
long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), period);
}
// 这个方法的主要作用是完善任务信息,并将任务添加到任务队列中
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// 对period进行数值约束,防止后续用到period时出现数值溢出
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
// 先判断任务线程thread是否还计划继续添加新任务
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的相关信息
task.nextExecutionTime = time;
task.period = period;
// 将任务的状态表位计划中(已添加至任务队列)
task.state = TimerTask.SCHEDULED;
}
// 进行添加操作
queue.add(task);
if (queue.getMin() == task)
queue.notify();
}
}
3.2 任务取消
// 取消该定时器,清空任务队列。唤醒等待线程。
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify(); // In case queue was already empty.
}
}
3.3 任务清除
// 源码中无调用,是提供给外部使用的一个方法
public int purge() {
int result = 0;
synchronized(queue) {
for (int i = queue.size(); i > 0; i--) {
if (queue.get(i).state == TimerTask.CANCELLED) {
queue.quickRemove(i);
result++;
}
}
if (result != 0)
queue.heapify();
}
return result;
}
快速移除任务队列中所有被取消的任务,并重新堆化队列,返回删除的任务个数。Timer中并没有定义删除子任务
的方法。而唯一可以删除的形式,就是设定子任务状态,然后调用purge()方法进行一次洗牌。这种做法和JVM GC
中标记回收有点异曲同工之处。倘若将回收的方法,公开出来,则Timer内部需要提供很健壮的任务管理机制,防
止在高并发的情况下,队列维持的堆不会出现数据错误,或性能问题(想一下如果有大量的移除操作,那么每个移
除操作都需要同步队列,然后重新堆化)。
TimerThread
1. 成员变量
字面义是“能否添加新任务”,默认为true,该字段为ture且任务队列中已经不存在任务时,我们可以优雅的结束定时器。注意,这个字段受到队列对象监视器的保护,当它被设置为false时(可能是任务取消也可能是线程被意外杀死)意味着当前Timer已经不存在实例对象,所以定时器在执行任务时,需要确保其值为true
boolean newTasksMayBeScheduled = true;
任务队列
private TaskQueue queue;
2. 构造方法
TimerThread(TaskQueue queue) {
this.queue = queue;
}
3. 核心方法
public void run() {
try {
mainLoop();
} finally {
// 如果线程意外终止,则同样需要走取消逻辑,清空队列,并标记此timer不再允许添加任务
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
TaskQueue 任务队列
1. 成员变量
优先级队列,它维护了一个最小堆结构(本文不具体介绍,可以简单理解为就是父节点都小于子节点的这样一棵树。而根节点就是下次运行时间最小的任务,注意,这个队列的起始下标为1,第一个任务被放在queue[1]的位置上,与我另一篇对堆结构的分析文章中有所不同,所以在算法逻辑上虽然无区别,但细节上有细微的差别。比如节点n的左右子节点为2n和2n+1,父节点为n>>1,如果不关注算法的话也可以不用细究)
private TimerTask[] queue = new TimerTask[128];
任务的实际数量
private int size = 0;
2. 主要方法
2.1 添加任务
void add(TimerTask task) {
// 扩容机制
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
// 注意,这里保证了第一个任务被添加时是放在下标1的位置上,下标0是不存放task的
queue[++size] = task;
// 确保将此任务按顺序放在合适的位置
fixUp(size);
}
2.2 获取优先级最高的任务
也就是下次预期执行时间最早的,在queue中排在首位的(下标0不存放task,优先级最高的是1)
TimerTask getMin() {
return queue[1];
}
2.3 重新设定下一次执行时间
void rescheduleMin(long newTime) {
queue[1].nextExecutionTime = newTime;
fixDown(1);
}
2.4 调整堆结构
// 以向上调整的方式调整顺序,以维持堆结构
private void fixUp(int k) {
while (k > 1) {
// 父节点j
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;
}
}
// 以向下调整的方式调整顺序,以维持堆结构
private void fixDown(int k) {
int j;
// 子节点j
while ((j = k << 1) <= size && j > 0) {
// 比较左右子节点,选出最小的
if (j < size &&
queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
j++; // j indexes smallest kid
// 跟父节点k进行比较,如果k较小,不需要交换,停止循环
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
// 如果进行了交换,则需要确保交换后,以原先子节点为父节点的结构也满足堆结构,继续循环直至完全满足
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
2.5 重新堆化
// 堆化
void heapify() {
// 从最后一个父节点开始,遍历所有父节点,执行向下调整操作,以重新构建堆结构(在timer.purge中被调用,即堆中元素发生了大量删除时需要重新构建堆)
for (int i = size/2; i >= 1; i--)
fixDown(i);
}
TimerTask 定时任务
public abstract class TimerTask implements Runnable {...}
这个类是一个继承自接口Runnable的抽象类,需要实现类自己去补充run方法,也就是自定义任务内容。
这个类比较简单,简单介绍一下
// 用于内部保证同步逻辑的一个锁变量
final Object lock = new Object();
// 状态变量,,初始状态为virgin。只有这个状态的任务才可以添加到queue中,子任务添加后,会改变子任务
// 的状态,所以子任务不会被反复多次添加到queue中
int state = VIRGIN;
static final int VIRGIN = 0; // 初始化
static final int SCHEDULED = 1; // 任务被添加到queue中即会设置该状态
static final int EXECUTED = 2; // 被执行过,只有不反复循环的子任务会被设置该状态
static final int CANCELLED = 3; // 被取消
// 下次被执行的时间(维持最小堆的判断标准)
long nextExecutionTime;
// 周期,初始是0毫秒,即不被反复执行。注意,该值区分正负,负数表示,每执行完一次后,间隔指定时间再次
// 执行。正数,表示每次开始执行后,指定间隔再次执行,前者用的比较多。
// nextExecutionTime = task.period<0 ? currentTime - task.period : executionTime + task.period
long period = 0;
// 取消任务
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
// 获取任务的下一次计划执行时间
public long scheduledExecutionTime() {
synchronized(lock) {
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}
上面两个方法在JDK源码中都没有被调用,是供外部调用的。
此部分转载于https://www.cnblogs.com/jilodream/p/5887110.html
最后的最后,来谈谈Timer类的定位:
(1)前Timer时代。
Timer是jdk1.3的时候,添加进源码的。这个时候大概是2000年左右。具体java被推出,才仅仅过去5年,所以1.3的主要改进,表现在新增的大量类库上。而在此之前,想拥有一个如Timer般的定时功能,是非常麻烦的,基本都要手动去实现。
(2)后Timer时代
查看了Timer的源代码之后,我们发现Timer在使用中存在这么问题:
1、定时任务是顺序执行的,也就是说后续的任务,一定要等到前边的任务执行完毕后,才会执行,否则将会一直等待。(其实这一点说不上来好还是坏,因为有时候我们可能会希望尽管是定时任务,但是执行时是有顺序完成和开始的,是要保证先后顺序的)
2、对系统时间非常敏感,通过代码我们知道,在每次子任务被取出后(执行run前),都会计算一遍执行时间,同时在判定子任务的执行时间是否已经到来时,都是直接获取到系统时间。倘若系统时间发生了修改,而使用的计划时间仍然是使用上次修改前的时间段时,就会出现一些意想不到的结果。如计划是5秒后执行,主线程wait 5秒钟后,被唤醒,在这5秒钟内,系统时间向后推迟了1天,那么主任务,仍然会执行该子任务(其他的也都会依次迅速执行,因为时间已经过了)。而倘若向前调整一天,那么主线程判断的时间仍然是,调整时间前的时间点,所以需要再等待一天。因此会出(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )现很多人以为Timer在调整时间后,被挂起,但是查看线程状态,发现还存在的奇怪场景。
3、子任务之间存在依赖。其实子任务之间的依赖关系并不强,无非就是前边的子任务执行完后,后边的子任务才可以开始执行。但是倘若在执行某个子任务时,捕捉到了异常,那么线程会立刻结束执行,后续的子任务都不会执行了,这个问题有时会对我们造成很大的困扰。
为了解决以上种种在jdk1.5中提供了ScheduledExecutorService接口以供开发者使用。
这个接口的实现,主要是通过线程池的形式,解决了上述遇到的问题(线程池也是jdk 1.5时才推出的),很多人因此认为Timer已经过时了,我觉得完全没有必要这样认为,通过自己对比Timer的原理和ScheduledExecutorService的改进之后。我们发现很多地方Timer仍然是有自己存在的必要的,只是占用场景不如ScheduledExecutorService多罢了。关于ScheduledExecutorService的学习,此处不再罗列,有兴趣的同学可以自己学习。