Timer
Timer定时器是一个从当前时间往后延期某段时间长度后才执行某些任务的一个工具类,作用类似于Thread.sleep方法,或者一种线程设施,用于安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。在实际的项目开发过程中,两个系统进行集成时,一个系统需要获取另一个系统的业务数据,此时系统需要周期性的调用集成系统的数据传送接口,而实际的被集成系统在线上实时的收集业务数据,那么使用Timer定时器周期性的调用数据收集任务,并把收集的数据write到database以便系统后续操作。
Timer使用Demo
两个任务Task1、Task2周期性的往控制台打印,而这两个任务执行的重复执行的时间间隔相同都是1000ms,而第一个是2000ms后执行,第二个1000ms后执行。
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
public void run() {
System.out.println("TimerTask1");
}
}, 2000, 1000);
timer.schedule(new TimerTask() {
public void run() {
System.out.println("TimerTask2");
}
},1000, 1000);
}
}
/*
*TimerTask2
*TimerTask2
*TimerTask1
*TimerTask2
*TimerTask2
.
.
.
*/
Timer的API
Timer是一个class,它的API结构在JDK的各个版本的帮助文档中都能找到,这里截取JDK1.5中的API描述。其中主要就是cancel、purge和schedule方法,他们作为Timer中任务列表queue的管理方法,也是Timer定时的管理方法以及任务调度的方法。
Timer框架执行结构
Timer类作为一个定时器执行框架,它在执行功能的过程中必然涉及到一些类间的相互作用。如下图所示:
从图中看到Timer类的几个属性nextSerialNumber(int)、queue(TaskQueue)、thread(TimerThread)和threadReaper(Object)。由这五个状态来反映出Timer类是如何进行任务管理。 在Timer类的源代码中,nextSerialNumber是作为Timer对象的命名的一部分而存在的。
private static synchronized int serialNumber() {
return (nextSerialNumber++);
}
public Timer()
{
this("Timer-" + serialNumber());
}
queue任务队列是一个TaskQueue对象,该对象的内部封装了一个TimerTask数组。TaskQueue其实就可以看成一个TimerTask的容器,它是一个线性数组实现的队列,只能在队尾插入,而在对头获取、删除。对于TaskQueue的源码中可以看出。
TaskQueue()
{
this.queue = new TimerTask[128]; //初始化的容器大小为128
this.size = 0; //队列中元素个数初始为0
}
//往任务列中增加任务
void add(TimerTask paramTimerTask)
{
//当元素个数超过队列的初始大小时,翻倍扩充容量
if (++this.size == this.queue.length) {
TimerTask[] arrayOfTimerTask = new TimerTask[2 * this.queue.length];
System.arraycopy(this.queue, 0, arrayOfTimerTask, 0, this.size); //将原容器中的0~this.size的元素拷贝到新容器中
this.queue = arrayOfTimerTask;
}
this.queue[this.size] = paramTimerTask;
fixUp(this.size);
}
//获取对头
TimerTask getMin()
{
return this.queue[1];
}
//删除
void removeMin()
{
this.queue[1] = this.queue[this.size];
this.queue[(this.size--)] = null;
fixDown(1);
}
Timer类执行任务是在一个子线程中进行执行,thread属性只有一个,也就是说Timer类中的任务都共享一个线程,对于它们来说,就是一个串行调用。
public Timer(String paramString, boolean paramBoolean)
{
this.queue = new TaskQueue();
this.thread = new TimerThread(this.queue); //将任务队列传递给线程对象执行
this.threadReaper = new Object() { //线程收割者
protected void finalize() throws Throwable {
synchronized (Timer.this.queue) {
Timer.this.thread.newTasksMayBeScheduled = false;
Timer.this.queue.notify();
}
}
};
this.thread.setName(paramString);
this.thread.setDaemon(paramBoolean);
this.thread.start();//启动线程
}
- threaReaper线程收割者是一个局部类,主要承担的工作是当垃圾回收时不允许添加新的任务到任务队列queue中,同时唤醒任务队列的对象上的等待线程队列。
TimerTask定时任务
任务implements了Runnable接口,同时自身带着四个属性:lock(Object)、nextExecutionTime(long)、period(long)和state(int)。查看其源码不难发现,lock是用于TimerTask的内部锁,当对TimerTask进行操作时,首先通过synchronized锁定该对象。
public boolean cancel()
{
synchronized (this.lock) //锁定对象
{
boolean bool = this.state == 1;
this.state = 3;
return bool;
}
}
public long scheduledExecutionTime()
{
synchronized (this.lock) //锁定对象
{
return this.period < 0L ? this.nextExecutionTime + this.period : this.nextExecutionTime - this.period;
}
}
nextExecutionTime和period这两个属性用于控制任务执行时间,从而决定任务是否在线程轮询中调用。state是任务执行过程中的对象可能会存在的状态:0(当前任务还没有被调用)、1(当前任务被调用了,如果是非重复的任务,则该任务还没有执行)、2(当前非重复任务已经执行了,或者在执行,但是还没有cancel)和3(该任务被cancel了)。
TimerThread定时器调度线程
TimerThread继承了Thread类,目的就是在进行thread.start时进行一些Timer特定的操作。在TimerThread中的除了构造函数外,只有一个函数mainLoop,它内部有个无限循环for(;;),用于轮询Timer中的任务队列,当队列为空时阻塞queue。queue.getMin取queue对头的task任务,同时锁定该任务进行执行。首先判断task的state状态值,从而决定对该任务的进一步动作(删除queue.removeMin,调用task.run,还是重复调用)。
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) {
}
}
}
Timer定时器调度任务
从上面的定时器的demo和API中可以看出schedule是Timer的调度方法,那么在schedule的内部又是如何工作?它首先检查传入的时间是否合法,如果传入的delay延迟时间小于零,也就是不可能发生事件,则抛出IllegalArgumentException异常。而其工作的核心为sched。
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
private void sched(TimerTask task, long time, long period) {
if (time < 0) //事件不可能发生时,则抛出异常
throw new IllegalArgumentException("Illegal execution time.");
// period的值不能无穷大
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;
synchronized(queue) {
//newTaskMayBeSheduled是告诉我们在queue中已经没有TimerTask可以操作了
if (!thread.newTasksMayBeScheduled)
throw new IllegalStateException("Timer already cancelled.");
//先锁定task任务,从保证线程安全
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中
queue.add(task);
//从任务队列queue中取任务,如果是刚放入则,则唤醒queue,此时queue非空。
if (queue.getMin() == task)
queue.notify();
}
}
Timer定时器缺陷
1、timer.schedule(TimerTask, executeTime, period)方式执行任务,每次在mainLoop的轮询中比较executionTime<=currentTime。currentTime取系统当前时间,而executionTime是当前任务的执行时间。Timer定时器中的任务队列中存在多个任务task,而每个任务的执行时间都是确定的,当mainLoop中选择的任务task执行时,则说明当前task的executionTime和系统currentTime是相等的。所以存在,一个任务task1在执行过程中占据很长的一段时间t1,预计执行时间c1,它没有执行完成时(因为Timer是单线程的,也就是该线程内的任务是串行执行),其他的任务都得等待。当task1执行完成后,执行时间task2要执行一段比较短的时间t2,预计执行时间c2,此时currentTime - task2.executionTime > 0,但是task2.period的步长不够,同时c2 - c1又没有足够大,那么task2跳过很多次后才执行。整个任务队列的执行都按照这样一种公式排位:task1.time + n * taskPeriod <= task2.time + m * taskPeriod,两者谁小谁先和currentTime进行比较,从而得到任务执行权。
2、在mainLoop中的通过try{……}catch(InterruptedException e) {}方式吃掉了线程异常,也就是在任务队列中的某个task如果发生了异常导致timerThread线程出现中断,那么此时定时器中的其他任务(已经被调度、等待被调度)都将失去执行机会,这种现象就叫做“线程泄漏”。也就是Timer定时器存在线程泄漏问题。
mainLoop() {
......
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 fired; run it, holding no locks
task.run();
......
}
void rescheduleMin(long newTime) {
queue[1].nextExecutionTime = newTime;//重设任务执行时间
fixDown(1); //将当前任务重新计算排位
}
注意
timer.cancel()不能忘记,因为当main中的主线程new Timer()时,创建了一个子线程执行任务,当任务队列queue中的任务task的state为canceled时,此时没有任务执行。而子线程却没有中断,则main主线程不会中断,也就导致资源浪费,程序不会终止。