定时调度任务Timer源码分析
概念
基于给定的时间点,给定的时间间隔或者给定的执行次数自动执行任务
Timer由JDK提供,由后台程序执行
而Quartz是开源项目,能力更强,采用线程池技术
Timer
有且仅有一个后台线程,能够执行多个任务,这些任务可以是一次性的以及定时的。注意一般这些任务的执行时间不能太长,不然会占用这个线程太久,从而延迟其他任务的执行。默认情况下,该线程不会是守护线程,因此能够保持线程不会结束,如果想结束一个timer的任务,可以调用cancel方法。
守护线程:是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。
如果定时器任务异常中断,比如调用了stop方法,再对该定时器尝试任务调度会抛出异常。
Timer的类是线程安全的,多个线程可以操作一个Timer对象。
Timer不保证任务的实时性。JDK5之后,使用ScheduledThreadPoolExecutor更好
使用注意:
内部对任务的排序是通过堆排序
调用任何的构造函数都会启动一个线程
Timer 定时调用TimerTask
内部有个TimerThread以及有TaskQueue
番外篇:监视器(monitor)的概念
下面这些琐碎的概念摘自其他博客:
监视器用来监视线程进入运行时状态,他确保同一时间只能有一个线程可以访问数据和代码,实际就是监视临界区只有一个线程访问
Monitor是一个同步工具,相当于操作系统中的互斥量(mutex),即值为1的信号量
它内置于每一个Object对象中,相当于一个许可证。拿到许可证即可以进行操作,没有拿到则需要阻塞等待。
syncrhoized利用monitor来实现加锁解锁,故syncrhoized又叫做内置锁
现在我们知道为什么用syncrhoized(lock)来加锁时,锁对象可以是任意对象了:
1:syncrhoized(lock)加锁时,用到的其实只是lock对象内置的monitor而已;
2:一个对象的monitor是唯一的,相当于一个唯一的许可证。拿到许可证的线程才可以执行,执行完后释放对象的monitor才可以被其他线程获取。
如果一个线程拥有了某些数据的锁,其他的线程则无法获得锁,直到这个线程释放了这个锁。在多线程中,如果任何时候都是我们自己来写这个信号量,显然不是很方便,幸运的是,JVM为我们自动实现了这些
监视器和锁在Java虚拟机中是一块使用的。监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。每一个监视器都和一个对象引用相关联。线程在获取锁之前不允许执行同步代码
wait方法和monitor的关系
wait让线程进入等待状态,直到其他的线程使用该对象调用notify()或者notifyAll()方法,源码中实际调用的是wait(0)方法。
当前的线程必须拥有该对象的monitor才能调用该方法。线程释放monitor的所有权后会等待,其他的线程调用notif或者notifyAll方法来通知这些等待的线程醒来。当线程重新获取到monitor的所有权之后才会继续执行。
notify方法和monitor的关系
notify会唤醒一个正在等待获取一个对象monitor的线程。如果有多个等待的线程,它们中的一个会被唤醒。同样调用该方法也需要获取该对象的monitor。
想拥有一个对象的monitor有三种方式(包括前面的wait方法也一样):
执行对象的同步方法
执行对象的同步块
对于对象是class来说,执行该class类的静态同步方方法
API
void | schedule(TimerTask task, long delay) | 安排任务,指定延时 |
---|---|---|
void | schedule(TimerTask task, long delay, long period) | 指定时延和周期 |
void | schedule(TimerTask task, Date time) | 指定时间,如果time已经过去会立即执行 |
void | schedule(TimerTask task, Date firstTime, long period) | 指定第一次时间和周期 |
void | scheduleAtFixedRate(TimerTask task, long delay, long period) | 固定频率,指定时延和周期 |
void | scheduleAtFixedRate(TimerTask task,Date firstTime, long period) | 固定频率,指定时延和周期 |
void | cancel() | 终止定时任务 |
int | purge() | 清理已经取消的任务,减少内存占用 |
后面两个是固定频率的,而前面的带peroid表示都是固定时延的,固定频率和固定时延的区别在于,固定频率在任务延期后会出现爆发性的执行,固定时延则不是。因此长时间看固定频率的定时任务执行的次数要比固定时延的次数多。(实现的区别详见下面的源码)
源码分析
构造函数:
public Timer() {
this("Timer-" + serialNumber());
}
这里会生成一个顺序生成的数字,内部使用的是AtomicInteger.getAndIncrement(),保证线程安全。实际调用的构造方法如下,会直接启动了线程:
public Timer(String name) {
thread.setName(name);
thread.start();
}
内部有个thread变量,它的类型是内部定义的一个线程类:
显然这是定时任务实现的核心部分,内部通过一个while(true)的循环实现,该循环不断的获取TimerTask并执行。
任务的添加和执行,实际是多线程中的生产者和消费者模式,需要进行加锁以及使用wait和notify实现
任务类TimerTask有一个lock对象,用来控制对TimerTask类的访问。TimerTask内部定义了几种状态:
VIRGIN= 任务没有被调度
SHEDULED: 任务被调度,如果是重复性的任务,同时表示该任务没有被执行
EXECUTED:表示非重复性任务已经被执行过或者正在执行
CANCELLED:表示任务被取消了
class TimerThread extends Thread {
//这个变量表示是否还有Timer对象的引用,如果没有就会终端Timer的线程
boolean newTasksMayBeScheduled = true;
private TaskQueue queue;
TimerThread(TaskQueue queue) {
this.queue = queue;
}
//线程启动调用的方法
public void run() {
try {
//主要的逻辑
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // 清除废弃的引用
}
}
}
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
//如果队列是空的时候并且还存在引用
while (queue.isEmpty() && newTasksMayBeScheduled)
//如果队列为空以及还有需要调度的,线程进入等待状态
//当有任务加入的时候,Timer会调用notify唤醒该线程
queue.wait();
//当被唤醒后,如果为空,就停止结束任务
if (queue.isEmpty())
//停止整个Timer
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();
//这里加锁是防止状态被变更,以及能够调用wait方法
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(
//固定延时和固定频率的区别所在
//peroid小于0表示是固定时延,基于当前时间计算
//peroid大于0表示固定频率,基于上次的执行时间
//因此如果线程延期或本身执行时间就大于周期时间,
//会出现多次或一直executionTime<=currentTime
//从而任务fire多次,即执行多次直到赶上进度
//当然也可能永远赶不上进度
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) {
}
}
}
}
队列的实现(采用二叉堆):
class TaskQueue {
//用数组实现二叉堆,理解上要当成一个二叉堆来看待
private TimerTask[] queue = new TimerTask[128];
private int size = 0;
int size() {
return size;
}
void add(TimerTask task) {
//当数组要满,便进行扩充
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);
}
//上浮操作,在数组末尾增加元素需要上浮
//这里面右移表示除以2
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;
}
}
//下沉操作,修改第一个元素要进行下沉操作
//左移表示乘以2
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);
}
}
Timer的缺点
对时效性要求较高的多任务并发作业支持不够
从代码中我们也能看到,每一都是先执行一个任务之后再去执行另一个任务,因此不能保证并发
对复杂任务的调度支持不够