前言
之前一直很好奇java的定时器到底是怎么做的?是启动了一个线程,隔一段时间执行一下传入的方法吗(不是)?怎么实现隔一段时间执行一次的逻辑的,是sleep一定时间吗(不是)?
现在终于知道看一下源码了,没想到逻辑竟然如此简单。。。
例子
import java.util.Timer;
import java.util.TimerTask;
public class Test {
public static void main(String[] args) {
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 0, 1000);
t.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("world!");
}
}, 500, 1000);
}
}
输出:
only once
hello
world!
hello
world!
hello
world!
源码概况
涉及到四个类:Timer、TimerThread、TimerQueue、TimerTask
Timer:定时器,里面包含几种schedule方法,可以调度任务
TimerThread:继承自Thread,是一个线程;Timer里有一个TimerThread的对象,这个对象是真正的定时器执行器(也就是说定时器其实是一个线程,这部分猜想是对的)
TimerQueue:一个队列,队列里保存的是TimerTask,也就是要执行的任务。队列是用最小堆的方式组织的
TimerTask:要调度的任务,其实就是实现了Runnable接口的一个抽象类(run方法留给子类实现,所以是抽象类);里边增加了任务调度状态的字段,增加了cancel方法用于定时器使用。
Timer类详述
最主要的两个变量:TaskQueue queue 和 TimerThread thread;queue用来保存所有要调度的任务,thread是一个单独的线程,对queue里的任务进行调度。
下面这个threadReaper 也很有意思,是终结方法守卫者,在为了在gc执行的时候做到优雅退出的方法。不详细介绍了,留下参考文章
/**
* This object causes the timer's task execution thread to exit
* gracefully when there are no live references to the Timer object and no
* tasks in the timer queue. It is used in preference to a finalizer on
* Timer as such a finalizer would be susceptible to a subclass's
* finalizer forgetting to call it.
*/
private final Object threadReaper = new Object() {
protected void finalize() throws Throwable {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.notify(); // In case queue is empty.
}
}
};
构造方法
几个构造方法,其实就两个变量,一个是给thread命名,另一个决定是否给thread设置为后台进程
然后就启动thread线程。
schedule方法
schedule,顾名思义,就是调度方法,用来调度任务的。
这里有三类方法:一类是只调度一次、一类是循环调度(用系统时间)、一类是循环调度(用既定时间)。开头的例子中就分别用了这三种方法。
后两种比较绕,简单解释一下就是:如果一个任务是循环调度的,当这个任务在某次调度中产生了延迟时,下次调度应该什么时候开始?一种是当前任务调度的时候,用系统时间加上调度间隔就是下次调度时间;另一种就是用任务本次本来该调用的时间+调度间隔,也就是它本来下次该被调度的时间,如果本次调度结束就满足了下次调度时间,那就立马开始下次调度。
那么,这两种方法是怎么区分的呢?我们可以看到,内部period一个为负一个为正来区分的,也是奇技淫巧了😀
public void schedule(TimerTask task, Date firstTime, long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), -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);
}
我们可以看到,内部其实都是调用的sched方法,逻辑上就是把task加入到队列queue中
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();
}
}
TimerThread类
这个类就是真正进行任务定时调度的地方了。有点ThreadPoolExecutor的味道。
有两个变量:boolean newTasksMayBeScheduled 这个是用来标识定时器是否结束的,Timer的cancel任务里会把它置为false;
TaskQueue queue 这是上面Timer里的任务队列,在Timer里传递进来的。
真正的执行环节:其实就是一个死循环,检查queue队列是否有需要执行的任务。如果queue是空的,就会调用queue的wait方法,这个方法会释放外面的锁,这样timer里就可以往queue里添加任务并唤醒queue上等待的线程(也就是当前这个TimerThread)了。
public void run() {
try {
mainLoop(); //主要实现内容
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
/**
* 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
//如果队列为空并且是有标志位,则等待。没有标志位的情况为不在需要执行timer了,比如cancel或被gc的时候
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();//获取就近的task
synchronized(task.lock) {
//如果该task已经被置为cancelled,则将它从队列里面移出
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) { // period表示该task是一次性的,用完就移出
queue.removeMin();//移出task,这块会有queue的重新排序
task.state = TimerTask.EXECUTED;//更新状态为执行中
} else {
//可重复执行的task操作,将重新计算下次执行时间,并重新排序
//重点,此处解释为什么period分正负:区别schedule方法和scheduleAtFixedRate
//如果是负数,则以当前时间为准,往后计算下次执行时间
//如果是正数,则以理论时间为准,往后计算下次执行时间
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // 如果还没到任务执行时间就处于等待
queue.wait(executionTime - currentTime);
}
if (taskFired) // 到执行时间了
//执行task中的run方法,而不是start方法,所以并不是另起一个线程进行操作
task.run();
} catch(InterruptedException e) {//如果是不能捕获的异常,就会有风险了
}
}
}
TaskQueue类
用一个数组,以最小堆的形式保存所有待调度的任务。里边的方法都是最小堆的实现形式,不细讲了,有空自己写一些最小堆。
TimerTask类
要调度的任务,其实就是实现了Runnable接口的一个抽象类(run方法留给子类实现,所以是抽象类);里边增加了任务调度状态的字段,增加了cancel方法用于定时器使用。
参考文章
代码剖析,写的很好:https://blog.csdn.net/xixi_haha123/article/details/81082321
终结方法守卫者:https://blog.csdn.net/xl890727/article/details/80082604