当一个 Timer 运行多个 TimerTask 时,只要其中一个 TimerTask 在执行中向 run 方法外抛出了异常,则其他任务也会自动终止。

问题的产生

这里做了一个小的 demo 来复现问题,代码如下。

  1. public class TestTimer {
  2. //创建定时器对象
  3. static Timer timer = new Timer();
  4. public static void mainString[] args {
  5. //添加任务 1,延迟 500ms 执行
  6. timer.schedule(new TimerTask() {
  7. @Override
  8. public void run() {
  9. System.out.println(「---one Task---」);
  10. try {
  11. Thread.sleep(1000);
  12. } catch InterruptedException e {
  13. e.printStackTrace();
  14. }
  15. throw new RuntimeException(「error 」);
  16. }
  17. }, 500);
  18. //添加任务 2,延迟 1000ms 执行
  19. timer.schedule(new TimerTask() {
  20. @Override
  21. public void run() {
  22. for (; {
  23. System.out.println(「---two Task---」);
  24. try {
  25. Thread.sleep(1000);
  26. } catch InterruptedException e {
  27. // TODO Auto-generated catch block
  28. e.printStackTrace();
  29. }
  30. }
  31. }
  32. }, 1000);
  33. }
  34. }

如上代码首先添加了第一个任务,让其在 500ms 后执行。然后添加了第二个任务在 1s 后执行,我们期望当第一个任务输出—-one Task—-后,等待 1s,第二个任务输出—-two Task—-,但是执行代码后,输出结果为

  1. ---one Task---
  2. Exception in thread "Timer-0" java.lang.RuntimeException: error
  3. at com.zlx.Timer.TestTimer$1.run(TestTimer.java:22)
  4. at java.util.TimerThread.mainLoop(Timer.java:555)
  5. at java.util.TimerThread.run(Timer.java:505)

Timer 实现原理分析

下面简单介绍 Timer 的原理,如图 11-9 所示是 Timer 的原理模型。

使用 Timer 时需要注意的事情 - 图1

图 11-9

● TaskQueue 是一个由平衡二叉树堆实现的优先级队列,每个 Timer 对象内部有一个 TaskQueue 队列。用户线程调用 Timer 的 schedule 方法就是把 TimerTask 任务添加到 TaskQueue 队列。在调用 schedule 方法时,**long delay** 参数用来指明该任务延迟多少时间执行。

● TimerThread 是具体执行任务的线程,它从 TaskQueue 队列里面获取优先级最高的任务进行执行。需要注意的是,只有执行完了当前的任务才会从队列里获取下一个任务,而不管队列里是否有任务已经到了设置的 delay 时间。一个 Timer 只有一个 TimerThread 线程,所以可知 Timer 的内部实现是一个多生产者-单消费者模型

从该实现模型我们知道,要探究上面的问题只需研究 TimerThread 的实现就可以了。TimerThread 的 run 方法的主要逻辑代码如下。

  1. public void run() {
  2. try {
  3. mainLoop();
  4. } finally {
  5. // Someone killed this Thread, behave as if Timer cancelled
  6. synchronizedqueue {
  7. newTasksMayBeScheduled = false
  8. queue.clear(); // Eliminate obsolete references
  9. }
  10. }
  11. }
  12. private void mainLoop() {
  13. while true {
  14. try {
  15. TimerTask task
  16. boolean taskFired
  17. //从队列里面获取任务时要加锁
  18. synchronizedqueue {
  19. ...
  20. }
  21. if taskFired
  22. task.run(); //执行任务
  23. } catchInterruptedException e {
  24. }
  25. }
  26. }

当任务在执行过程中抛出 InterruptedException 之外的异常时,唯一的消费线程就会因为抛出异常而终止,那么队列里的其他待执行的任务就会被清除。所以在 TimerTask 的 run 方法内最好使用 try-catch 结构捕捉可能的异常,不要把异常抛到 run 方法之外其实要实现 Timer 功能,使用 ScheduledThreadPoolExecutor 的 schedule 是比较好的选择。如果 ScheduledThreadPoolExecutor 中的一个任务抛出异常,其他任务则不受影响。

  1. public class TestScheduledThreadPoolExecutor {
  2. static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new
  3. ScheduledThreadPoolExecutor(1);
  4. public static void main(String[] args) {
  5. scheduledThreadPoolExecutor.schedule(new Runnable() {
  6. @Override
  7. public void run() {
  8. System.out.println("---one Task---");
  9. try {
  10. Thread.sleep(1000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. throw new RuntimeException("error ");
  15. }
  16. }, 500, TimeUnit.MICROSECONDS);
  17. scheduledThreadPoolExecutor.schedule(new Runnable() {
  18. @Override
  19. public void run() {
  20. for (int i =0; i<2; ++i) {
  21. System.out.println("---two Task---");
  22. try {
  23. Thread.sleep(1000);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. }, 1000, TimeUnit.MICROSECONDS);
  30. scheduledThreadPoolExecutor.shutdown();
  31. }
  32. }

运行结果如下。

使用 Timer 时需要注意的事情 - 图2

之所以 ScheduledThreadPoolExecutor 的其他任务不受抛出异常的任务的影响,是因为在 ScheduledThreadPoolExecutor中的 ScheduledFutureTask 任务中 catch 掉了异常,但是在线程池任务的 run 方法内使用 catch 捕获异常并打印日志是最佳实践。

小结

ScheduledThreadPoolExecutor 是并发包提供的组件,其提供的功能包含但不限于 TimerTimer 是固定的多线程生产单线程消费,但是 ScheduledThreadPoolExecutor 是可以配置的,既可以是多线程生产单线程消费也可以是多线程生产多线程消费,所以在日常开发中使用定时器功能时应该优先使用 **ScheduledThreadPoolExecutor**