当一个 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() {
@Override
public 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() {
@Override
public void run() {
for (; ; ) {
System.out.println(「---two Task---」);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}, 1000);
}
}
如上代码首先添加了第一个任务,让其在 500ms 后执行。然后添加了第二个任务在 1s 后执行,我们期望当第一个任务输出—-one Task—-后,等待 1s,第二个任务输出—-two Task—-,但是执行代码后,输出结果为
---one Task---
Exception in thread "Timer-0" java.lang.RuntimeException: error
at 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 cancelled
synchronized(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 = new
ScheduledThreadPoolExecutor(1);
public static void main(String[] args) {
scheduledThreadPoolExecutor.schedule(new Runnable() {
@Override
public 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() {
@Override
public 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**
。