Timer定时器因修改系统时间导致挂起的原因

时间:2022-06-25 23:36:29

文章同步发布在朗度云网站,传送门:http://www.wolfbe.com/detail/201608/5.html


系统中经常需要做一些定时的任务,比如榜单排行、定时缓存等。在java中我们会经常用到Timer来做定时器,用来运行定时任务。Timer的用法很简单,只要定义好间隔时间和任务函数,Timer实例就会按指定间隔时间重复地执行任务函数。

也许你在使用Timer的过程中很顺利,但有可能你也会发现Timer会无缘无故地挂起,不再重复地执行任务了。

在我的一个系统运行过程中,我发现了一个问题,一旦修改系统时间,把系统时间调到当前时间之后(即大于当前时间T1)T2,Timer线程正常执行;如果再将系统时间修改到当前时间之前T3(即T3小于T2),那么Timer线程就会挂起,或者假死,此时不会再执行定时任务,好像线程已经死掉一样。

下面用一个Timer的例子来重复一下上面的过程,代码如下:

public static void main(String [] args) {
   Timer timer = new Timer();
   timer.schedule(new TimerTask() {
       public void run(){
           System.out.println("They are v.wolfbe.com and sou.wolfbe.com!");
           System.out.println("current time is: "+System.currentTimeMillis());
       }
   }, 0, 5*1000);
}

Timer定时5秒的间隔在控制台打印两句话,运行main函数后,如果什么都不改,那么程序会5秒的间隔不停地在控制台打印出:

They are v.wolfbe.com and sou.wolfbe.com!
current time is:142222552XXX
They are v.wolfbe.com and sou.wolfbe.com!
current time is:142222552XXX

假如当前时间为2016-7-15 18:20:20,那么现在把时间调整为2016-7-16 18:20:20,此时程序还是不停地打印上面的消息。

当我再把时间调整为2016-7-15 18:20:20后,程序突然就不打印消息了,感觉Timer线程死掉了 ,但我们可以发现此时main程序还是没有退出,Timer线程还在运行,证明Timer线程只是挂起了,而没有死掉。


那么问题是为什么Timer线程会挂起了呢,而不再重复执行任务了???

我们可以从Timer的实现源码中找下原因,从schedule方法进去,一直追踪调试下去,可以找到:

public void run() {
   try {
       mainLoop();
   } finally {
       // Someone killed this Thread, behave as if Timer cancelled
       synchronized(queue) {
           newTasksMayBeScheduled = false;
           queue.clear();  // Eliminate obsolete references
       }
   }
}

Timer的实现原理原来就是在一个线程中执行mainLoop()函数,再看下mainLoop()函数源码:

/**
* The main timer loop.  (See class comment.)
*/
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) {
       }
   }
}

代码中出现了while(true),说明一直在重复地执行,可以猜测我们的任务代码就是放在while(true)里面执行的。确定是这样,我们的任务被放在了一个queue的队列中,mainLoop方法就是不断地执行队列里面所有的任务。


//获取当前系统时间
currentTime = System.currentTimeMillis();
//下一次执行任务的时间
executionTime = task.nextExecutionTime;

taskFired = (executionTime<=currentTime)


如果下一次执行任务的时间小于等于当前时间,证明执行任务的时间已经到了,向下执行任务,如果不是,即taskFired=false,那么就等待,等待的时间为executionTime - currentTime:

if (!taskFired) // Task hasn't yet fired; wait
      queue.wait(executionTime - currentTime);


如果我们把系统时间调整为未来的时间,那么executionTime<=currentTime肯定为true,执行下次任务,程序运行正常;

如果我们再次调整系统时间为之前的时间,那么此时executionTime<=currentTime肯定为false,那么就会执行代码queue.wait(executionTime - currentTime),此时线程就会挂起了,至于等待的时间明显为executionTime - currentTime。


也可以用具体的时间来说明一下,假如当前时间为2016-07-15 10:20:20,调整时间为2016-07-16 10:20:20,等待执行一次任务后,此时的时间假如为2016-07-16 10:20:28,那么executionTime = task.nextExecutionTime=2016-07-16 10:20:33;此时再次调整时间为2016-07-15 10:20:33,那么Timer线程需要等待一天的时间才会再次执行任务了,是不是很神奇呢?

很奇怪的是jdk一直都没有修复Timer这个bug,所以在使用Timer的时候一定要确保不能随意修改系统时间,否则后果不堪设想。如果想避免上面的问题,可以使用Quartz框架来做定时任务。