使用Timer执行定时任务很简单,一般这样子写:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("hello world");
}
};
timer.schedule(task, 10, 2000);
以上代码创建了一个定时器和定时任务,大部分情况下它都能正常工作,延迟10ms后,每隔2s就打印一个hello world。
但其实Timer是有一些问题的,程序中如果不注意就可能出现问题,下面来自《Java并发编程实战》:
1.Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间长度大于其周期时间长度,那么就会导致这一次的任务还在执行,而下一个周期的任务已经需要开始执行了,当然在一个线程内这两个任务只能顺序执行,有两种情况:对于之前需要执行但还没有执行的任务,一是当前任务执行完马上执行那些任务(按顺序来),二是干脆把那些任务丢掉,不去执行它们。至于具体采取哪种做法,需要看是调用schedule还是scheduleAtFixedRate。
2.如果TimerTask抛出了一个未检出的异常,那么Timer线程就会被终止掉,之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
下面通过分析Timer的源码来解释为什么会有上面的问题。
Timer的实现原理很简单,概括的说就是:Timer有两个内部类,TaskQueue和TimerThread,TaskQueue其实就是一个最小堆(按TimerTask下一个任务执行时间点先后排序),它存放该Timer的所有TimerTask,而TimerThread就是Timer新开的检查兼执行线程,在run中用一个死循环不断检查是否有任务需要开始执行了,有就执行它(注意还是在这个线程执行)。
因此Timer实现的关键就是调度方法,也就是TimerThread的run方法:
public void run() {
try {
mainLoop();
} finally {
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear();
}
}
}
具体逻辑在mainLoop方法中实现:
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break;
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue;
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currenttime)) {="" if="" (task.period="=" 0)="" queue.removemin();="" task.state="TimerTask.EXECUTED;" }="" else="" queue.reschedulemin(="" task.period<0="" ?="" currenttime="" -="" task.period="" :="" executiontime="" +="" task.period);="" (!taskfired)="" queue.wait(executiontime="" currenttime);="" (taskfired)="" task.run();="" catch(interruptedexception="" e)="" }<="" code="">
从第14行开始,这里取出那个最先需要执行的TimerTask,然后22行判断executionTime<=currenttime,其中executiontime就是该timertask下一个周期任务执行的时间点,currenttime为当前时间点,如果为true说明该任务需要执行了(注意可能是一个过时任务,应该在过去某个时间点开始执行,但由于某种原因还没有执行),接着第23行判断task.period ==”0,Timer中period默认为0表示该TimerTask只会执行一次,不会周期性地不断执行,所以为true那么就移除掉该TimerTask,然后待会会执行该TimerTask一次。如果task.period不为0,那就分为小于0和大于0,如果调用的是schedule方法:
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);="" }<="" code="">
那么period就小于0,如果调用的是scheduleAtFixedRate方法:
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);="" }<="" code="">
那么period就大于0。
回到mainLoop方法中,当period<0时,当前timertask下一次开始执行任务的时间就会被设置为currenttime -=”” task.period,可理解为定时任务被重置,从现在开始,period周期间隔(那么之前预想在这个间隔内存在的任务执行就没了)后执行第一次任务,这种情况就是timer的任务可能丢失问题。当period=””>0,当前TimerTask下一次开始执行任务的时间就会被设置为executionTime + task.period,即下一次任务还是按原来的算,因此如果这时executionTime + task.period还先于currentTime,那么下一个任务就会马上执行,也就是Timer的任务快速调用问题。
以上分析解释了第一点,下面解释第二点。
从代码上可以看到在死循环中只catch了一个InterruptedException,也就是当前线程被中断,因此Timer的线程是可以执行一段时间,然后被操作系统挂到一边休息,然后又回来继续执行的。但如果抛出其它异常,那么整个循环就挂掉,当然外层的run方法也没有catch任何异常:
public void run() {
try {
mainLoop();
} finally {
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear();
}
}
}
这时就会造成线程泄露,同时之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
补充:对于上面说到的Timer线程执行到一半被挂到一边去,这种情况与任务执行时间过长类似,如果调用schedule方法的话就有可能导致任务丢失。在Android中,有一种叫长连接的东西,它需要客户端发心跳包确保连接的存在,如果使用Timer实现定时发心跳包就可能会有问题,如果Timer线程在执行过程中被换出去了,那么调用schedule的就很有可能导致心跳包没有发出去,而调用scheduleAtFixedRate又可能会导致Timer线程没有占用CPU时心跳包没发出去,某一时刻又快速地发送好几个心跳包。因此在Android中一般使用AlarmManager实现心跳包的定时发送。
从JDK5开始引入了ThreadPoolExecutor,它可以用来实现定时任务,因此一般建议使用它来实现定时任务:
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5)
ses.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("hello world")
}
}, 10, 2000, TimeUnit.MILLISECONDS)