Timer的缺陷分析
Timer计时器可以定时(指定时间执行任务)、延迟(延迟5秒执行任务)、周期性地执行任务(每隔个1秒执行任务),但是,Timer存在一些缺陷:
-
Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间长度大于其周期时间长度,那么就会导致这一次的任务还在执行,而下一个周期的任务已经需要开始执行了,当然在一个线程内这两个任务只能顺序执行,有两种情况:对于之前需要执行但还没有执行的任务,一是当前任务执行完马上执行那些任务(按顺序来),二是干脆把那些任务丢掉,不去执行它们。至于具体采取哪种做法,需要看是调用schedule还是scheduleAtFixedRate。
-
如果TimerTask抛出了一个未检查的异常,那么Timer线程就会被终止掉,之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
-
Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感。
源码分析
下面通过分析Timer的源码来解释为什么会有上面的问题。
Timer的实现原理很简单,概括的说就是:Timer有两个内部类,TaskQueue和TimerThread,TaskQueue其实就是一个最小堆(按TimerTask下一个任务执行时间点先后排序),它存放该Timer的所有TimerTask,而TimerThread就是Timer新开的检查兼执行线程,在run中用一个死循环不断检查是否有任务需要开始执行了,有就执行它(注意:任务还是在这个线程执行)。
因此Timer实现的关键就是调度方法,也就是TimerThread的run方法:
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
具体逻辑在mainLoop方法中实现:
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
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();
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(
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) {
}
}
}
从第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);
}
那么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);
}
那么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 {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
这时就会造成线程泄露,同时之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。
补充:对于上面说到的Timer线程执行到一半被挂到一边去,这种情况与任务执行时间过长类似,如果调用schedule方法的话就有可能导致任务丢失。在Android中,有一种叫长连接的东西,它需要客户端发心跳包确保连接的存在,如果使用Timer实现定时发心跳包就可能会有问题,如果Timer线程在执行过程中被换出去了,那么调用schedule的就很有可能导致心跳包没有发出去,而调用scheduleAtFixedRate又可能会导致Timer线程没有占用CPU时心跳包没发出去,某一时刻又快速地发送好几个心跳包。因此在Android中一般使用AlarmManager实现心跳包的定时发送。
例子
下面举几个例子,演示一下Timer的缺陷。
问题一
Timer在执行定时任务时只会创建一个线程,如果存在多个任务,若其中某个任务因为某种原因而导致任务执行时间过长,超过了两个任务的间隔时间,会发生一些缺陷:
public class TimerTest04 {
private Timer timer;
public long start;
public TimerTest04() {
this.timer = new Timer();
start = System.currentTimeMillis();
}
public void timerOne() {
timer.schedule(new TimerTask() {
public void run() {
System.out.println("timerOne invoked ,the time:" + (System.currentTimeMillis() - start));
try {
Thread.sleep(4000); // 线程休眠4000
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 1000);
}
public void timerTwo() {
timer.schedule(new TimerTask() {
public void run() {
System.out.println("timerOne invoked ,the time:" + (System.currentTimeMillis() - start));
}
}, 3000);
}
public static void main(String[] args) throws Exception {
TimerTest04 test = new TimerTest04();
test.timerOne();
test.timerTwo();
}
}
按照我们正常思路,timerTwo应该是在3s后执行,其结果应该是:
timerOne invoked ,the time:1001
timerOne invoked ,the time:3001
但是事与愿违,timerOne由于sleep(4000),休眠了4S,同时Timer内部是一个线程,导致timeOne所需的时间超过了间隔时间,结果:
timerOne invoked ,the time:1000
timerOne invoked ,the time:5000
问题二
如果TimerTask抛出RuntimeException,Timer会终止所有任务的运行。如下:
public class TimerTest04 {
private Timer timer;
public TimerTest04(){
this.timer = new Timer();
}
public void timerOne(){
timer.schedule(new TimerTask() {
public void run() {
throw new RuntimeException();
}
}, 1000);
}
public void timerTwo(){
timer.schedule(new TimerTask() {
public void run() {
System.out.println("我会不会执行呢??");
}
}, 1000);
}
public static void main(String[] args) {
TimerTest04 test = new TimerTest04();
test.timerOne();
test.timerTwo();
}
}
运行结果:timerOne抛出异常,导致timerTwo任务终止。
Exception in thread "Timer-0" java.lang.RuntimeException
at com.chenssy.timer.TimerTest04$1.run(TimerTest04.java:25)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
用ScheduledExecutorService替代Timer
对于Timer的缺陷,我们可以考虑使用 ScheduledThreadPoolExecutor 来替代。Timer是基于绝对时间的,对系统时间比较敏感,而ScheduledThreadPoolExecutor 则是基于相对时间;Timer是内部是单一线程,而ScheduledThreadPoolExecutor内部是个线程池,所以可以支持多个任务并发执行。
解决问题一
public class ScheduledExecutorTest {
private ScheduledExecutorService scheduExec;
public long start;
ScheduledExecutorTest(){
this.scheduExec = Executors.newScheduledThreadPool(2);
this.start = System.currentTimeMillis();
}
public void timerOne(){
scheduExec.schedule(new Runnable() {
public void run() {
System.out.println("timerOne,the time:" + (System.currentTimeMillis() - start));
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},1000,TimeUnit.MILLISECONDS);
}
public void timerTwo(){
scheduExec.schedule(new Runnable() {
public void run() {
System.out.println("timerTwo,the time:" + (System.currentTimeMillis() - start));
}
},2000,TimeUnit.MILLISECONDS);
}
public static void main(String[] args) {
ScheduledExecutorTest test = new ScheduledExecutorTest();
test.timerOne();
test.timerTwo();
}
}
运行结果:
timerOne,the time:1003
timerTwo,the time:2005
解决问题二
public class ScheduledThreadPoolDemo01 {
public static void main(String[] args) throws InterruptedException {
final TimerTask task1 = new TimerTask() {
@Override
public void run() {
throw new RuntimeException();
}
};
final TimerTask task2 = new TimerTask() {
@Override
public void run() {
System.out.println("task2 invoked!");
}
};
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.schedule(task1, 100, TimeUnit.MILLISECONDS);
pool.scheduleAtFixedRate(task2, 0, 1000, TimeUnit.MILLISECONDS);
}
}
代码基本一致,但是ScheduledExecutorService可以保证,task1出现异常时,不影响task2的运行:
task2 invoked!
task2 invoked!
task2 invoked!
task2 invoked!
task2 invoked!
综上所述,基本说明了在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer。
网友评论