说到任务调度,大家可能会想到Quartz框架,但是jdk自带的简单任务调度工具类,反而了解的人并不是很多。我觉得如果你的业务相对简单的话,没必要非得用Quartz等框架,使用Timer完全可以胜任的。简单来分享一下我了解的Timer。
Timer是jdk中提供的一个定时器工具,使用的时候会在主线程之外起一个单独的线程执行指定的计划任务,可以指定执行一次或者反复执行多次。
一、Timer 使用
Timer 调度任务有一次性调度和循环调度,循环调度有分为固定速率调度(fixRate)和固定时延调度(fixDelay)。固定速率强调准点,固定时延强调间隔。
如果你有一个任务必须每天准点调度,那就应该使用固定速率调度,并且要确保每个任务执行时间不要太长,千万别超过了第二天这个点。如果你有一个任务需要每隔几分钟跑一次,那就使用固定时延调度,它不是很在乎你的单个任务要跑多长时间。
常用方法
//0表示立即执行一次,以后每隔一段时间执行一次
timer.schedule(task, 0, PERIOD);
timer.schedule(task, bookTime(10,45,0), 10000)
//1000表示1秒后执行一次,以后每隔一段时间执行一次
timer.schedule(task, 1000, PERIOD);
//0表示立即执行一次,以后每隔一段时间执行一次
timer.schedule(task, 1000, PERIOD);
// 在当天14点4分整,执行一次,以后不再执行
timer.schedule(task, bookTime(15,0,0));
//在当天10点34分整,执行一次,以后每隔一段时间执行一次
//如果时间超过了设定时间,会立即执行一次
timer.schedule(task, bookTime(0,34,10),PERIOD);
timer.scheduleAtFixedRate(task, bookTime(0,40,0),PERIOD);
代码示例
我是用TimerTask来创建一个任务,其中run方法里是任务调度的逻辑。使用一个Timer对象来调度任务。
TimerTask是一个实现了Runnable接口的抽象类,代表一个可以被Timer执行的任务。
public class TaskManager {
private static final long PERIOD = 5 * 1000;// 1秒钟
public TaskManager() {
Timer timer = new Timer();
System.out.println("start");
InsertFlagInTxt insertTimeTask= new InsertFlagInTxt();
MySqliteTask myTask = new MySqliteTask();
MyTxtLogTask myLogTask = new MyTxtLogTask();
System.out.println("start");
//触发更新 insert timetimer = new Timer();
timer.scheduleAtFixedRate(insertTimeTask, bookTime(14,04,0), 24*60*60*1000);
//触发更新txt de logtimer = new java.util.Timer(true);
timer.schedule(myLogTask, 0, 60*60*1000);
//触发更新数据库的timertimer = new java.util.Timer(true);
timer.schedule(myTask,0,60*60*1000);
}
private Date bookTime(int hour, int minute, int second) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute);
calendar.set(Calendar.SECOND, second);
Date date = calendar.getTime();
return date;
}
/*public static void main(String[] args) {new TaskManager();}*/
}
Task任务代码
publicclassInsertFlagInTxtextendsTimerTask{privatestaticbooleanisRunning=false;publicvoidrun(){if(!isRunning){isRunning=true;System.out.println("开始将上传失败日志导入到数据库!");try{System.out.println("業務代碼");isRunning=false;}catch(NumberFormatExceptione){isRunning=true;e.printStackTrace();}catch(IOExceptione2){e2.printStackTrace();}System.out.println("指定任务执行结束");}else{System.out.println("上一次任务执行还未结束");}}}
二、内部结构
Timer 类里包含一个任务队列和一个异步轮训线程。任务队列里容纳了所有待执行的任务,所有的任务将会在这一个异步线程里执行,切记任务的执行代码不可以抛出异常,否则会导致 Timer 线程挂掉,所有的任务都没得执行了。单个任务也不易执行时间太长,否则会影响任务调度在时间上的精准性。比如你一个任务跑了太久,其它等着调度的任务就一直处于饥饿状态得不到调度。所有任务的执行都是这单一的 TimerThread 线程。
class Timer {
TaskQueue queue = new TaskQueue();
TimerThread thread = new TimerThread(queue);
}
Timer 的任务队列 TaskQueue 是一个特殊的队列,它内部是一个数组。这个数组会按照待执行时间进行堆排序,堆顶元素总是待执行时间最小的任务。轮训线程会每次轮训出时间点最近的并且到点的任务来执行。数组会自动扩容,如果任务非常多。
class TaskQueue {
TimerTask[] queue = new TimerTask[128];
int size;
}
任意线程都可以通过 Timer.schedule 方法将任务加入 TaskQueue,但是 TaskQueue 又并不是线程安全的数据结构。所在每次修改 TaskQueue 时都需要加锁。
synchronized(queue) {
...
}
任务状态
TimerTask 有 4 个状态,
VIRGIN 是默认状态,刚刚实例化还没有被调度。
SCHEDULED 表示已经将任务塞进 TaskQueue 等待被执行。
EXECUTED 表示任务已经执行完成。
CANCELLED 表示任务被取消了,还没来得及执行就被人为取消了。
abstract class TimerTask {
int state = VIRGIN;
static final int VIRGIN = 0;
static final int SCHEDULED = 1;
static final int EXECUTED = 2;
static final int CANCELLED = 3;
long nextExecutionTime; // 下次执行时间 long period = 0; // 间隔}
对于一个循环任务来说,它不存在 EXECUTED 状态,因为它每次刚刚执行完成,就被重新调度了。EXECUTED 状态仅仅存在于一次性任务,而且这个状态其实并不是表示任务已经执行完成,它是指已经从任务队列里摘出来了,马上就要执行。
任务间隔字段 period 比较特殊,当使用固定速率时,period 为正值,当使用固定间隔时,period 为负值,当任务是一次性时,period 为零。下面是循环任务的下次调度时间设定
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
// 固定时延基于 currentTime 顺延
// 固定速率基于 executionTime(设定时间) 顺延
// next_exec_time = exec_time + period = first_delay + n * period
queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period);
对于固定速率来说,如果任务执行时间太长超出了间隔,那么它可能会持续霸占任务队列,因为它的调度时间将总是低于 currentTime,排在堆顶,每次轮训取出来的都是它。运行完毕后,重新调度这个任务,它的时间依旧赶不上。持续下去你会看到这个任务的调度时间远远落后于当前时间,而其它任务可能会彻底饿死。这就是为什么一定要特别注意固定速率的循环任务运行时间不宜过长。
垃圾回收
还有一个特殊的场景需要特别注意,那就是当轮训线程因为队列里没有任务而睡眠的时候,Timer 对象因为不再被引用而被垃圾回收了。这时候需要主动唤醒轮训线程,让它退出
三、常见异常
在Java中要定时执行一个任务,有很多童鞋自然会想到用Timer,但是Timer如果使用不当,会造成以下问题:
1.每new一个Timer会启动一个线程,如果使用在循环或者递归当中,很容易造成JVM报如下错误:
Exception in thread "Timer-2" java.lang.OutOfMemoryError: Java heap space
在JVM中如果98%的时间是用于GC且可用的 Heap size 不足2%的时候将抛出此异常信息。
java.lang.OutOfMemoryError: unable to create new native thread
这个时候机器上的内存并没有耗尽,JVM有一定的保护机制,具体可以参见:
2.Timer中执行的任务,如果有异常抛出,timer就会终止,后续的任务不会再执行,推荐使用ScheduledExecutorService。
阿里推荐的编码规约中这样写道:
多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。
//org.apache.commons.lang3.concurrent.BasicThreadFactory
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
//do something
}
},initialDelay,period, TimeUnit.HOURS);