1) 与每个 Timer 对象相对应的是单个后台线程,用于顺序地执行所有计时器任务。计时器任务应该迅速完成。如果完成某个计时器任务的时间太长,那么它会“独占”计时器的任务执行线程。因此,这就可能延迟后续任务的执行,而这些任务就可能“堆在一起”。 --摘自jdk1.6文档
2) 如果某个任务抛出了异常,整个Timer会停止,后继任务都不能被执行
下面笔者通过编程来重现以上两个问题。
/** * Timer执行周期任务,任务间隔5秒 */ public class Test { public static void main(String[] args) { Timer timer = new Timer(); //周期任务,任务间隔5s timer.schedule(task, 0, 5*1000); } static TimerTask task = new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000L); //注释1. 暂停10s,延长任务执行时间 // try { // Thread.sleep(10 * 1000); // } catch (Exception e) { // } // throw new RuntimeException("execption test");//注释2. 异常测试。 } } }
执行结果如下:
说明 | 结果 | 分析 |
直接执行 | 任务执行时间戳间隔为5秒,与预期相符 | |
打开注释1,延长任务执行时间 | 任务执行间隔为10秒,与预期不符。印证了问题1). | |
关闭注释1,打开注释2. | 只打印了一个时间就抛出了异常 |
执行任务抛出异常,程序退出,但不要因此而小看这个问题,如果把它放在servlet容器(如tomcat)或在某个框架中运行,虽然抛出了异常但程序很可能会继续执行的,如果异常没有记录到日志中,那你就头疼去吧。 |
通过查看Timer源码我们可以发现Timer本质上是将所有要进行调度的TimerTask放进一个根据下次执行时间nextExecutionTime排序的队列中(TaskQueue实例,它最大能存储128个TimerTask),然后通过一个线程(TimerThread实例)来循环执行此队列中的任务。因此Timer调度的任务是在一个线程中执行的。而如果其中某一任务运行时间大于任务间执行间隔,会阻塞后续任务,所以Timer的定时在时间上是不严格的。而且对于任务执行时出现的异常也没有处理,所以它也是不可靠的。这两个问题也存在于ScheduledExecutorService的scheduleAtFixedRate方法中。
如下是TimerThread的主循环方法:
/** * 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) { } } }
你觉得下面这段代码有没有问题?
public class Test { public static void main(String[] args) { //取得一个10个线程的线程池 ScheduledExecutorService executor = Executors.newScheduledThreadPool(5); for(int i=0; i<100; i++){ //每隔200ms执行一次 executor.schedule(new ProblematicCommand(), 200, TimeUnit.MILLISECONDS); } //加入调度的线程任务全部执行后关闭线程池,程序退出。 executor.shutdown(); } static class ProblematicCommand implements Runnable{ static ThreadLocal<SomeResource> resources = new ThreadLocal<SomeResource>(){ @Override protected SomeResource initialValue() { System.out.printf("[Thread-%2d] init resource\r\n", Thread.currentThread().getId()); return new SomeResource(); } }; public void releaseResource(){ resources.get().release(); } public void run() { resources.get().use(); releaseResource(); //label 1: 释放资源 } } static class SomeResource{ private boolean released=false; //使用资源,如果资源已释放会抛出运行时异常 public void use() { if(!released) System.out.printf("[Thread-%2d] Functional Tool is used\r\n", Thread.currentThread().getId()); else throw new RuntimeException("resource has already been closed."); } //释放资源 public void release(){ this.released=true; System.out.printf("[Thread-%2d] resource released.\r\n", Thread.currentThread().getId()); } } }
我们将上面的代码运行起来后会发现它没有执行预期的次数(100次)就退出了,在我的电脑上运行结果为:初始化资源5次,使用5次,释放资源5次。
而如果把label 1代码注释掉,执行后发现代码只初始化了5次资源,但使用了100次!
因为使用了线程池,所以上面的那些任务是在特定的几个线程中执行的(即5个线程共执行了100次任务),这就导致了ThreadLocal变量中只储存了与线程池中的线程ID相关的线程变量(ThreadLocal可以简单理解为Map<线程ID, SomeResource>)。也就是说某一个线程中执行了多次任务,因此在这个线程中执行的多个任务使用的却是同一个线程ID, 这就是资源只初始化5次的原因。而如果在任务执行完成后释放了资源,那么在释放资源的线程中再次执行任务时,由于与该线程ID绑定的资源已经被释放,在使用资源时抛出了异常,而线程池在发现异常后会导致整个线程池,所以程序直接退出了。
有某任务需要循环执行,设其单次执行时间为t, 执行间隔为g,当t<g时定时器可以按时执行,但当t>=g时,任务被执行的间隔就成为了t.
另外在调度任务时要注意可能出现的异常。Timer在出现异常时会打印出异常栈,而ScheduledExecutorService却没有任何提示,但它们都会停止对任务的执行。
而在线程池中使用ThreadLocal,或通过定时器循环调度某一任务时(Timer可以看成是拥有一个线程的线程池)要注意ThreadLocal所持有的资源的状态。