在JDK 5.0之前,java.util.Timer/TimerTask是唯一的内置任务调度方法,而且在很长一段时间里很热衷于使用这种方式进行周期性任务调度。
本篇博文就先使用Timer/TimerTask来完成任务的调度。接着再来分析Timer/TimerTask的源码。
1、void cancel()
终止此计时器,丢弃所有当前已安排的任务。
2、 int purge()
从此计时器的任务队列中移除所有已取消的任务。
3、 void schedule(TimerTask task, Date time)
安排在指定的时间执行指定的任务。
4、 void schedule(TimerTask task, Date firstTime, long period)
安排指定的任务在指定的时间开始进行重复的固定延迟执行。
5、 void schedule(TimerTask task, long delay)
安排在指定延迟后执行指定的任务。
6、 void schedule(TimerTask task, long delay, long
period)
安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。
7、 void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
安排指定的任务在指定的时间开始进行重复的固定速率执行。
8、 void scheduleAtFixedRate(TimerTask task, long delay, long period)
安排指定的任务在指定的延迟后开始进行重复的固定速率执行。
首先我们定义了两个任务类,代码如下,所有关于Timer/TimerTask的Demo都是基于这两个Task。
class TimerTask1 extends TimerTask{
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void run() {
System.out.println(sdf.format(new Date())+" TimerTask1 begin running...,运行此任务的线程为:"+Thread.currentThread().getName());
try {
Thread.sleep(1000);//模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date())+" TimerTask1 over...,运行此任务的线程为:"+Thread.currentThread().getName());
}
}
class TimerTask2 extends TimerTask{
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void run() {
System.out.println(sdf.format(new Date())+" TimerTask2 begin running...,运行此任务的线程为:"+Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sdf.format(new Date())+" TimerTask2 over...,运行此任务的线程为:" +Thread.currentThread().getName());
}
}
TimerTask1采用睡眠1s来模拟任务的执行过程。TimerTask2采用睡眠2s来模拟任务的执行过程。
测试如下:
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask1(), 0);
timer.schedule(new TimerTask2(), 0);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
timer.cancel();
}
当定时器中添加了TimerTask1和TimerTask2两个任务,且都没有任何延时。
执行结果如下:
2016-08-05 10:44:36 TimerTask1 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:44:37 TimerTask1 over...,运行此任务的线程为:Timer-0
2016-08-05 10:44:37 TimerTask2 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:44:39 TimerTask2 over...,运行此任务的线程为:Timer-0
有运行结果可以得到的结论:
1、Timer类使用的是一个线程串行的执行提交的任务。
2、两个任务提交的执行的延时相等,则任务的执行顺序和提交的先后顺序一致。
既然任务的执行顺序和延时有关系,那么就看一个关于 有延时的例子,如下:
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask1(), 10);//延迟10ms后执行
timer.schedule(new TimerTask2(), 0);
timer.schedule(new TimerTask2(), 5);//延迟5ms
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
timer.cancel();
}
代码中的定时器提交了1个延时10ms执行的TimerTask1.提交了两个TimerTask2,这两个TimerTask2任务一个没有延时立即执行,一个延时5ms执行。
运行结果如下:
2016-08-05 10:49:55 TimerTask2 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:49:57 TimerTask2 over...,运行此任务的线程为:Timer-0
2016-08-05 10:49:57 TimerTask2 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:49:59 TimerTask2 over...,运行此任务的线程为:Timer-0
2016-08-05 10:49:59 TimerTask1 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:50:00 TimerTask1 over...,运行此任务的线程为:Timer-0
结论:
1、如果提交的任务存在延时,则会加入到任务队列中,并根据延迟时间进行排序执行。
Timer/TimerTask类还支持任务的周期性执行。我们也看一个例子
public static void main(String[] args) {
Timer timer = new Timer();
//1s之后开始执行并每隔1s执行一次
timer.scheduleAtFixedRate(new TimerTask1(), 1000, 1000);
timer.scheduleAtFixedRate(new TimerTask2(), 1000, 1000);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
timer.cancel();
}
定时器重复的执行TimerTask1和TimerTask2.
运行结果如下:
2016-08-05 10:55:54 TimerTask1 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:55:55 TimerTask1 over...,运行此任务的线程为:Timer-0
2016-08-05 10:55:55 TimerTask2 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:55:57 TimerTask2 over...,运行此任务的线程为:Timer-0
2016-08-05 10:55:57 TimerTask2 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:55:59 TimerTask2 over...,运行此任务的线程为:Timer-0
2016-08-05 10:55:59 TimerTask1 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:56:00 TimerTask1 over...,运行此任务的线程为:Timer-0
2016-08-05 10:56:00 TimerTask1 begin running...,运行此任务的线程为:Timer-0
2016-08-05 10:56:01 TimerTask1 over...,运行此任务的线程为:Timer-0
结论:
1、Timer执行任务是单线程的,内部用任务队列来维护待执行任务
2、任务使用最小堆算法排序(任务下次执行时间距今越小越优先被执行),添加任务时使用锁机制防止并发问题。
上述Task1,Task2任务的起始执行时间都为1000,周期间隔都为1000ms。因此同一个任务连续执行了两次
如果想将每个任务执行一次,则将延迟时间改为不一致即可。
根据上面的一些例子,我们用屁股都能想到,在Timer类中肯定有一个队列来维护任务的执行顺序,也有一个线程来执行队列中的任务,是吧。
源码中也确实是这样,有一个任务队列:TaskQueue,与任务队列绑定的线程TimerThread。
/*
任务队列与定时器线程(timer thread)配合使用。
timer通过调用schedule方法来将任务加入到任务队列taskQueue中,
timer thread在适当的时候执行队列中的任务并将此任务从任务队列中移除
*/
//任务队列,来维护任务的执行顺序
private final TaskQueue queue = new TaskQueue();
/**
* The timer thread.
*/
//任务线程,用来执行任务队列中的任务
private final TimerThread thread = new TimerThread(queue);
在研究任何类的源码,我们都是从其构造函数看起,这个类也不例外。
Timer类中有4个构造函数,如下:
//创建一个定时器,且与之相关的线程不是daemon线程
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线程的名字我们可以自己指定,如果不指定就采用程序自己的方式给线程命名。命名方式如下:
/**
* This ID is used to generate thread names.
*用于产生线程的名字
*/
private final static AtomicInteger nextSerialNumber = new AtomicInteger(0);
private static int serialNumber() {
return nextSerialNumber.getAndIncrement();
}
采用了一个AtomicInteger来给线程命名。例如:在上面例子中我们看到Timer-0就是线程的名字。
看完构造函数之后,就到了我们分析的重点了,schedule方法的内部实现。
/*
* Schedules the specified task for execution after the specified delay.
翻译:安排指定的任务在指定的延时之后执行
*/
public void schedule(TimerTask task, long delay) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
sched(task, System.currentTimeMillis()+delay, 0);
}
/*
* Schedules the specified task for execution at the specified time. If
* the time is in the past, the task is scheduled for immediate execution.
*翻译:安排指定的任务在指定的时间指定,如果指定的时间过去了,则立即执行
*/
public void schedule(TimerTask task, Date time) {
sched(task, time.getTime(), 0);
}
/*
*翻译:安排指定的任务根据固定延时的重复的执行,第一次执行的时间为给定的延时。
*如果有一个因为垃圾回收等其他因素而被延时执行,则后面的也顺着延时执行
*/
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);
}
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -period);
}
上面是Timer类中4中重载的schedule的方法,这四种方法最终都是调用了sched(TimerTask task, long time, long period)方法,因此我们的重点就回归到了这个方法上。不过要注意的是:
Timer类中所有的schedule方法都调用了sched方法,而sched方法的第二个参数就是基于绝对时间的。即Timer类是基于绝对时间来完成任务的调用执行的。
Timer类中sched(TimerTask task, long time, long period)源码如下:
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
// Constrain value of period sufficiently to prevent numeric
// overflow while still being effectively infinitely large.
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);//将当前任务加入队列
//判断当前任务是否是最小的
if (queue.getMin() == task)
queue.notify();
}
}
/**
* Adds a new task to the priority queue.
*/
void add(TimerTask task) {
// Grow backing store if necessary
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;
}
}
上面的代码逻辑比较清楚
1、将任务加入到任务队列中,并改变任务的状态
2、调整队列,由于队列是采用最小堆来实现,即保证队列的第一个元素为最短时间的,即即将要被执行的。
以上就是schedule方法的内部实现,是不是比较简单。
看到这里,我们还不知道队列queue中的任务是怎么被线程执行的呢,是吧,下面我们就来看下,我们都知道,在Timer类的构造函数中,执行任务的线程就已经启动,因此,我们自然而然的会想到去看下TaskThread类的run方法到底干了写什么??
看一看任务队列中的任务是如何一个一个被线程执行的,开始揭秘。
TaskThread类的run方法的代码如下:
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
run方法中主要调用了mainLoop方法。
值得注意的是:当mainLoop抛出任何异常时,线程将结束并将任务队列清空退出。
/**
* The main timer loop. (See class comment.)
*/
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);
}
//如果任务可以被执行,则立即执行这个任务的run方法。
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
}
mainLoop方法中的思想也比较简单:
1、拿出任务队列中的第一个任务,如果执行时间还没有到,则继续等待,否则立即执行
这里要注意的是:如果是一次性任务,则移除任务并更改任务的状态为EXECUTE.,如果是周期执行,则在队列中将此任务不移出,只更改任务的下一次执行时间并调整任务队列。
以上就是Timer/TimerTask的源码分析。看过源码之后,确实比较简单哈。忘记说了,Timer类中的任务队列的底层采用数组来进行时间的。
Timer/TimerTask也比较好用哈,但是有如下的一些缺点,
1、Timer的任务是单线程来执行的,即只有一个线程来执行所有的任务
2、Timer类是基于绝对时间来实现的任务调度。
3、正是由于Timer只有一个线程来按顺序执行任务,当某一个任务执行失败而抛异常,则会导致后面所有等待执行的线程全部不能被执行。
java.util.concurrent.ScheduledExecutorService的出现正好弥补了Timer/TimerTask的缺陷。关于ScheduledExecutorService由于篇幅的限制,就留的下篇博文再介绍,不过在结束之前,看两个例子
scheduleAtFixedRate的使用
scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit):建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。如果任务的任何一个执行遇到异常,则后续执行都会被取消。否则,只能通过执行程序的取消或终止方法来终止该任务。如果此任务的任何一个执行要花费比其周期更长的时间,则将推迟后续执行,但不会同时执行。
public class ScheduledExecutorServerDemo {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
public static void main(String[] args) {
//创建一个线程数为5的线程池,支持周期执行任务
ScheduledExecutorService exec = Executors.newScheduledThreadPool(5);
exec.scheduleAtFixedRate(new Runnable(){
@Override
public void run() {
try {
System.out.println(sdf.format(new Date())+" "+Thread.currentThread().getName()+" task is running..");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
};
}
}, 1, 1, TimeUnit.SECONDS);//以1s为周期来运行此任务
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
exec.shutdown();
}
}
运行结果:
2016-07-05 05:07:34 pool-1-thread-1 task is running..
2016-07-05 05:07:36 pool-1-thread-1 task is running..
2016-07-05 05:07:38 pool-1-thread-2 task is running..
2016-07-05 05:07:40 pool-1-thread-2 task is running..
2016-07-05 05:07:42 pool-1-thread-3 task is running..
从结果可以得到如下结论
1、ScheduledExecutorService支持多线程来同时处理任务
2、当任务的运行时间比周期长时,则将推迟后续执行,但不会同时执行。这里的运行时间为2s,而周期为1s。结果中任务执行的间隔为2s就可以说明这一点
scheduleWithFixedDelay的使用
scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。如果任务的任一执行遇到异常,就会取消后续执行。否则,只能通过执行程序的取消或终止方法来终止该任务。
public class ScheduledExecutorServerDemo2 {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd hh:mm:ss");
public static void main(String[] args) {
//创建一个线程数为5的线程池,支持周期执行任务
ScheduledExecutorService exec = Executors.newScheduledThreadPool(5);
exec.scheduleWithFixedDelay(new Runnable(){
@Override
public void run() {
try {
System.out.println(sdf.format(new Date())+" "+Thread.currentThread().getName()+" task is running..");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
};
}
}, 1, 1, TimeUnit.SECONDS);//以1s为间隔重复运行
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
exec.shutdown();
}
}
运行结果:
2016-14-05 05:14:02 pool-1-thread-1 task is running..
2016-14-05 05:14:05 pool-1-thread-1 task is running..
2016-14-05 05:14:08 pool-1-thread-1 task is running..
从结果可以得到如下结论
1、scheduledWithFixedDelay方法是上一次任务结束到此次任务开始执行的间隔为delay.
从结果可以看出,任务执行的间隔为3s=任务执行时间2s+间隔delay(1s)