文章同步发布在朗度云网站,传送门:http://www.wolfbe.com/detail/201608/5.html
也许你在使用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框架来做定时任务。