当一个 Timer 运行多个 TimerTask 时,只要其中一个 TimerTask 在执行中向 run 方法外抛出了异常,则其他任务也会自动终止。
问题的产生
这里做了一个小的 demo 来复现问题,代码如下。
public class TestTimer {//创建定时器对象static Timer timer = new Timer();public static void main(String[] args) {//添加任务 1,延迟 500ms 执行timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println(「---one Task---」);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}throw new RuntimeException(「error 」);}}, 500);//添加任务 2,延迟 1000ms 执行timer.schedule(new TimerTask() {@Overridepublic void run() {for (; ; ) {System.out.println(「---two Task---」);try {Thread.sleep(1000);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}, 1000);}}
如上代码首先添加了第一个任务,让其在 500ms 后执行。然后添加了第二个任务在 1s 后执行,我们期望当第一个任务输出—-one Task—-后,等待 1s,第二个任务输出—-two Task—-,但是执行代码后,输出结果为
---one Task---Exception in thread "Timer-0" java.lang.RuntimeException: errorat com.zlx.Timer.TestTimer$1.run(TestTimer.java:22)at java.util.TimerThread.mainLoop(Timer.java:555)at java.util.TimerThread.run(Timer.java:505)
Timer 实现原理分析
下面简单介绍 Timer 的原理,如图 11-9 所示是 Timer 的原理模型。

图 11-9
● TaskQueue 是一个由平衡二叉树堆实现的优先级队列,每个 Timer 对象内部有一个 TaskQueue 队列。用户线程调用 Timer 的 schedule 方法就是把 TimerTask 任务添加到 TaskQueue 队列。在调用 schedule 方法时,**long delay** 参数用来指明该任务延迟多少时间执行。
● TimerThread 是具体执行任务的线程,它从 TaskQueue 队列里面获取优先级最高的任务进行执行。需要注意的是,只有执行完了当前的任务才会从队列里获取下一个任务,而不管队列里是否有任务已经到了设置的 delay 时间。一个 Timer 只有一个 TimerThread 线程,所以可知 Timer 的内部实现是一个多生产者-单消费者模型。
从该实现模型我们知道,要探究上面的问题只需研究 TimerThread 的实现就可以了。TimerThread 的 run 方法的主要逻辑代码如下。
public void run() {try {mainLoop();} finally {// Someone killed this Thread, behave as if Timer cancelledsynchronized(queue) {newTasksMayBeScheduled = false;queue.clear(); // Eliminate obsolete references}}}private void mainLoop() {while (true) {try {TimerTask task;boolean taskFired;//从队列里面获取任务时要加锁synchronized(queue) {...}if (taskFired)task.run(); //执行任务} catch(InterruptedException e) {}}}
当任务在执行过程中抛出 InterruptedException 之外的异常时,唯一的消费线程就会因为抛出异常而终止,那么队列里的其他待执行的任务就会被清除。所以在 TimerTask 的 run 方法内最好使用 try-catch 结构捕捉可能的异常,不要把异常抛到 run 方法之外。其实要实现 Timer 功能,使用 ScheduledThreadPoolExecutor 的 schedule 是比较好的选择。如果 ScheduledThreadPoolExecutor 中的一个任务抛出异常,其他任务则不受影响。
public class TestScheduledThreadPoolExecutor {static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = newScheduledThreadPoolExecutor(1);public static void main(String[] args) {scheduledThreadPoolExecutor.schedule(new Runnable() {@Overridepublic void run() {System.out.println("---one Task---");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}throw new RuntimeException("error ");}}, 500, TimeUnit.MICROSECONDS);scheduledThreadPoolExecutor.schedule(new Runnable() {@Overridepublic void run() {for (int i =0; i<2; ++i) {System.out.println("---two Task---");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}}, 1000, TimeUnit.MICROSECONDS);scheduledThreadPoolExecutor.shutdown();}}
运行结果如下。

之所以 ScheduledThreadPoolExecutor 的其他任务不受抛出异常的任务的影响,是因为在 ScheduledThreadPoolExecutor中的 ScheduledFutureTask 任务中 catch 掉了异常,但是在线程池任务的 run 方法内使用 catch 捕获异常并打印日志是最佳实践。
小结
ScheduledThreadPoolExecutor 是并发包提供的组件,其提供的功能包含但不限于 Timer。Timer 是固定的多线程生产单线程消费,但是 ScheduledThreadPoolExecutor 是可以配置的,既可以是多线程生产单线程消费也可以是多线程生产多线程消费,所以在日常开发中使用定时器功能时应该优先使用 **ScheduledThreadPoolExecutor**。
