前言
自从分析activiti5的源码之后,感觉对其使用打通了任督二脉一样。以前一直挺排斥看源码,因为效率低,看着也晦涩,总想着知道怎么用就好了。但是现在觉得如果只看如何操作,那是真的知其然不知其所以然。碰到基础的使用确实可以搞定,但稍微复杂点,来点变化就蒙了,如果有项目需要魔改(或者魔改了让你用),那只能硬着头皮看,只有看懂了源码,才能真正驾驭一个组件、一个类、一个知识点、一个框架。
为什么会看起Timer的源码来,因为最近使用一个组件bboss,用于从数据库导数据到ElasticSearch中。这其中定时任务会使用Timer。所以就决定看看Timer怎么使用,尤其是该组件通过设置可调用Timer的schedule或scheduleAtFixedRate方法。这两个方法有什么不同呢?这又激起了我追溯源码的兴趣。
定时器Timer的使用
Java中有很多任务调度的框架,例如QuartZ、xxl-job等等。而Timer的上手非常简单我们可以看一下具体的使用:
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
Task task = new Task();
timer.schedule(task, 1000L, 3000L);
}
}
class Task extends TimerTask{
@Override
public void run() {
System.out.println(new Date());
}
}
这个测试类,14-20行定义一个Task类继承TimerTask类,并且重写其run方法。TimerTask是实现了Runnable接口的。对于了解多线程的读者来说,应该很好理解。第8行新建一个Timer类,第9行新建一个Task类。第10行执行调度。schedule这个方法的意思是,1000毫秒之后,开始调度task这个任务,并且之后每隔3000毫秒调度一次。timer的调度方法有schedule的多个重载方法,以及scheduleAtFixedRate,具体使用都可以试试。这几个调度方法,两个入参的都是只执行一次,三个入参的则是重复执行。
Timer源码分析
先灌输一下概念,Timer的架构总体来说就是起一个线程,然后把需要调度的任务,按下次执行时间先后顺序排队,之后每次从队列里取第一个任务出来对比任务下次执行时间和当前时间,达到了当前时间则执行,否则等待。
先看Timer timer = new Timer();这句,创建的Timer源码:
public class Timer {
//任务队列
private final TaskQueue queue = new TaskQueue();
//调度任务的线程
private final TimerThread thread = new TimerThread(queue);
//......
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
//......
}
构造函数先为线程设个名字,然后开启线程执行。熟悉线程的读者,会想到接下来就是调用线程的run方法。我们接下来看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) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
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) {
}
}
}
}
10-19行的run方法,主要就是调用mainLoop。定时任务的线程,主要就是循环,循环至任务执行的时间,就执行任务。26行synchronized锁住任务队列queue,主要是避免别的线程同时添加或删除任务队列。27行判断如果队列为空,而且任务调度器没被取消(例如调用TImer的cancel方法),则进行等待,否则29行如果任务调度器被取消而且队列为空,即以处理完所有任务,则跳出循环。33行获取任务队列第一个任务,再次说明一下,任务队列是按下次执行时间排序的,因此第一个任务必然是最接近下次执行时间的任务。35-38行如果任务状态被设置为CANCELLED,则移出任务队列并继续循环。39、40行分别获取当前时间,以及该任务下次执行时间。41行当下次执行时间小于当前时间,则证明已经到达了任务执行时间,要执行任务。如果period参数是0,则只执行一次即可,如果否则要再次调度。
46-48行的设计比较巧妙,这是区分schedule和scheduleAtFixedRate方法的关键。通过schedule方法传入,最终给task设置period是个负数,而scheduleAtFixedRate是正数(两个方法在用户api调用时都是正数,内部处理时schedule变负数)。所以呈现出一加一减的情况。schedule的调度是按照当前时间+间隔时间作为下次调度的时间,而scheduleAtFixedRate则是按照本次计划调度时间+间隔时间作为下次调度时间。说起来有点绕口,举个例子,假设间隔时间是10秒,一个任务在第0秒时调度,正常是在第10秒处调度,如果第一次调度这个任务时处理了15秒,schedule方法设置下次调度的时间是第25秒,而scheduleAtFixedRate则会设置第20秒。如果第二次处理这个任务的时间变短了只需3秒,那scheduleAtFixedRate会在第20秒第三次调度这个任务,而schedule还是要等第25秒调度,文章后面会做个类似的实验。
第52行判断,如果没到执行任务的时间,则等待对应的时长。55行如果到了执行时间,则调用task的run方法。那就是调用我们用户自定义Task类重写的run方法。
一开始任务队列是空的,因此会一直卡在28行wait那里,需要等任务队列中的任务notify才能唤醒这个线程。接下来我们分析timer.schedule方法:
public class Timer {
//......
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 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);
}
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");
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();
}
}
//......
}
看下第9行和第18行,schedule和scheduleAtFixedRate的区别在于period取正还是取负。Timer通过这个巧妙的设计,少设置一个区分schedule和scheduleAtFixedRate的变量。25行如果设置period过大(大于Long最大值),则会右移一位,32-39行初始化task,并设置其为调度状态SCHEDULED。41行加入到任务队列中。42-43行判断若此时任务队列从零个任务到一个任务,则立即notify唤醒mainLoop的wait。
前面讲过,任务队列会按下次任务调度时间先后顺序进行排列,因此,任务队列TaskQueue在增加和移除任务时会重新排序。我们看下TaskQueue:
class TaskQueue {
private TimerTask[] queue = new TimerTask[128];
private int size = 0;
int size() {
return size;
}
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);
}
TimerTask getMin() {
return queue[1];
}
TimerTask get(int i) {
return queue[i];
}
void removeMin() {
queue[1] = queue[size];
queue[size--] = null; // Drop extra reference to prevent memory leak
fixDown(1);
}
void quickRemove(int i) {
assert i <= size;
queue[i] = queue[size];
queue[size--] = null; // Drop extra ref to prevent memory leak
}
void rescheduleMin(long newTime) {
queue[1].nextExecutionTime = newTime;
fixDown(1);
}
boolean isEmpty() {
return size==0;
}
void clear() {
// Null out task references to prevent memory leak
for (int i=1; i<=size; i++)
queue[i] = null;
size = 0;
}
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;
}
}
private void fixDown(int k) {
int j;
while ((j = k << 1) <= size && j > 0) {
if (j < size &&
queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
j++; // j indexes smallest kid
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
void heapify() {
for (int i = size/2; i >= 1; i--)
fixDown(i);
}
}
在调用schedule或scheduleAtFixedRate时,会把任务添加到任务队列中,就是调用TaskQueue的add方法。12-13行若容量达到上限则为任务队列分配两倍的容量。然后在任务队列的最后一项添加,再调用fixUp进行添加后排序。57-65行为堆排序,非常简洁,非常高效,值得学习。在排序算法中,快速排序和堆排序的平均时间复杂度都是O(nlogn),在排序算法中比较高效,对于新增元素,快排和堆排序都是O(logn),但是对于移除首个元素,快排要把数组整体前移一位,复杂度是O(n),而堆排序是O(logn),显然堆排序快一点。堆排序比较适合这类数组中频繁新增和移出元素的情况。同理67-78也是堆排序,减少某个元素之后的堆排序。(PS:有读者会觉得既然如此,让快排反过来排序,把下次计划执行时间最小的人排在最后,那移除元素的时候移除最后一个,就不需要移动任何元素。这个想法是对的,但是使用堆排序的原因在于,它可以满足移除任一元素,时间复杂度都为O(logn),而快排在最不理想的情况下是O(n))
关于schedule和scheduleAtFixedRate区别的实践
下面写了一个简单的例子,第一次先打开schedule,第二次试验打开scheduleAtFixedRate。这里设置为立即进行调度,调度间隔是10秒。运行的逻辑是第一次调度sleep阻塞15秒,模拟任务要执行15秒的时间。第二次及以后的调度阻塞3秒:
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
Task task1 = new Task();
timer.schedule(task1, 0L, 10000L);
// timer.scheduleAtFixedRate(task1, 0L, 10000L);
}
}
class Task extends TimerTask{
private static int count = 0;
@Override
public void run() {
System.out.println(new Date());
try {
if(count < 1) {
Thread.currentThread().sleep(15000L);
}else{
Thread.currentThread().sleep(3000L);
}
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
schedule的执行结果:
第一次是17:38:31调度,下次计划调度时间是17:38:41,因为执行任务是sleep阻塞了15秒,所以已经到17:38:46秒,此时会设置第三次调度的时间为10秒之后,即17:38:56。
接下来执行scheduleAtFixedRate查看结果:
第一次是17:40:26调度,下次计划调度时间是17:40:36,与schedule的情况一样,因为阻塞了15秒,到了17:40:41,而scheduleAtFixedRate的设置是按上次计划调度时间+时间间隔,所以下次任务调度的时间是17:40:36 + 10秒 = 17:40:46。
小结:
Timer就是开一个线程,然后按照计划执行时间逐个调度任务,当前一个任务未执行完之前,下一个任务即使到了计划执行时间也不会开始。因此需要并行的执行任务可以考虑放在不同的Timer中调度。另外schedule和scheduleAtFixedRate的选择方面,如果任务执行时间很小,而调度的频率不高的情况下,两种方式不会有什么明显差异,但在具体场景可能还是需要进行选择的。最后,源码展示了堆排序的实现方式,非常简洁,值得各位学习。