在JDK1.3中,新增了java.util.Timer来实现定时功能。它简单易用,但不足也非常明显。
- Timer内只有一个线程,因此所有任务都是串行执行的;
- 如果Timer中线程因异常退出,将不会创建新线程来继续执行任务;
- 前一个任务的延迟或异常,都将影响到之后的任务执行。
生产中,我们几乎不会使用Timer来做定时任务,更多使用定时线程池ScheduledExecutorService,框架如Quartz、xxl-job、elastic-job等。本文将为你学习它们打个基础。
1 定时器Timer
它有两个主要属性:任务队列TaskQueue和工作线程TimerThread。
创建Timer时:
- 默认名称为
"Timer-" + serialNumber; - 可以设置Thread为守护线程;
- 在调用
new Timer()时,将启动任务线程。
向Timer提交任务的方法有:
// 提交仅执行一次的延迟任务
public void schedule(TimerTask task, long delay)
// 提交周期任务
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
2 TimerThread
任务执行的工作线程,它从任务队列获取任务,在它们到期时执行它们。
- 仅执行一次的delay任务,到期执行后从队列中移除;
- 周期执行的定时任务,本次执行后将重新计算下次执行时间,再放回到队列中。
它有两个属性:
- newTasksMayBeScheduled,标识线程是否继续工作,默认为true;
- queue,任务队列。
run方法调用了mainLoop(),死循环来持续处理任务。
2.1 周期任务如何执行?
在mainLoop()中, 主要逻辑如下:
- 每次循环,只从队列中获取到期时间最早的一个任务;
- 根据task.executionTime<=currentTime,判断任务到期否;
- delay任务(task.period == 0),仅执行一次就从队列中移除;
- 周期任务在本次执行前,会更新task.executionTime为下次执行时间,并在队列中重排序;
- task.run是在工作线程中执行的;即使有多个任务同时到期,也得串行依次执行;
2.2 工作线程何时退出呢?
从源码中可以看到有4种情况:
- 任务抛出
InterruptedException之外的异常,不会被catch,进而结束。
private void mainLoop () {
while (true) {
try {
// 执行任务
} catch (InterruptedException e) {
}
}
}
- newTasksMayBeScheduled=false时,不会调用
queue.wait(),如果队列为空则break跳出死循环而结束; - 主动调用
Timer#cancel方法,清空队列,终止定时器。 new Timer()时设置工作线程为daemon的,主线程终止时Timer也退出。
3 TimerTask
TimerTask是Runnable接口的抽象子类,很简单,仅有如下属性:
// synchronized锁对象,更新task属性时需加锁
final Object lock = new Object();
// 状态,默认为VIRGIN即尚未被调度;
// 可取值还有SCHEDULED、EXECUTED、CANCELLED
int state = VIRGIN;
// 到期时间,毫秒值
long nextExecutionTime;
// 执行周期
long period = 0;
对task属性的修改,使用synchronized(task.lock)来防止并发。
4 TaskQueue
一个定时器任务的优先级队列,根据TimerTask的nextExecutionTime排序。在内部使用一个堆,它为add、removeMin和rescheduleMin操作提供log(n)性能,为getMin操作提供常数时间性能。
基于数组实现,初始大小为128,size到达上限时,扩容到两倍大小。
对queue的操作,使用
synchronized(queue)防止并发。
5 使用Timer时的注意事项
5.1 串行特征
下面代码中,task1和task2都延迟1秒后执行。由于task1耗时2秒,导致task2在第3秒时才被执行。
import lombok.extern.slf4j.Slf4j;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutionException;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestTimer")
public class TestTimer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");
sleep(2);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
log.debug("start...");
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
}
}
5.2 异常退出
还是上面的代码,只是在task1中抛出异常。结果TimerThread退出,不会执行task2。
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");
// 模拟业务异常
int i = 1 / 0;
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
log.debug("start...");
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);