timer 在 JDK 里面, 是很早的一个 API 了. 具有延时的, 并具有周期性的任务, 在 newScheduledThreadPool 出来之前我们一般会用 Timer 和 TimerTask 来做, 但是 Timer 存在一些缺陷, 为什么这么说呢?
Timer 只创建唯一的线程来执行所有 Timer 任务. 如果一个 timer 任务的执行很耗时, 会导致其他 TimerTask 的时效准确性出问题. 例如一个 TimerTask 每 10 秒执行一次, 而另外一个 TimerTask 每 40ms 执行一次, 重复出现的任务会在后来的任务完成后快速连续的被调用 4 次, 要么完全 "丢失"4 次调用. Timer 的另外一个问题在于, 如果 TimerTask 抛出未检查的异常会终止 timer 线程. 这种情况下, Timer 也不会重新回复线程的执行了; 它错误的认为整个 Timer 都被取消了. 此时已经被安排但尚未执行的 TimerTask 永远不会再执行了, 新的任务也不能被调度了.
这里做了一个小的 demo 来复现问题, 代码如下:
- package com.hjc;
- import java.util.Timer;
- import java.util.TimerTask;
- /**
- * Created by cong on 2018/7/12.
- */
- public class TimerTest {
- // 创建定时器对象
- 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---,
但是执行完毕代码后输出结果如下所示:
例子 2,
- public class Shedule {
- private static long start;
- public static void main(String[] args) {
- TimerTask task = new TimerTask() {
- public void run() {
- System.out.println(System.currentTimeMillis()-start);
- try{
- Thread.sleep(3000);
- }catch (InterruptedException e){
- e.printStackTrace();
- }
- }
- };
- TimerTask task1 = new TimerTask() {
- @Override
- public void run() {
- System.out.println(System.currentTimeMillis()-start);
- }
- };
- Timer timer = new Timer();
- start = System.currentTimeMillis();
- // 启动一个调度任务, 1S 钟后执行
- timer.schedule(task,1000);
- // 启动一个调度任务, 3S 钟后执行
- timer.schedule(task1,3000);
- }
- }
上面程序我们预想是第一个任务执行后, 第二个任务 3S 后执行的, 即输出一个 1000, 一个 3000.
实际运行结果如下:
实际运行结果并不如我们所愿. 世界结果, 是过了 4S 后才输出第二个任务, 即 4001 约等于 4 秒. 那部分时间时间到哪里去了呢? 那个时间是被我们第一个任务的 sleep 所占用了.
现在我们在第一个任务中去掉 Thread.sleep(); 这一行代码, 运行是否正确了呢? 运行结果如下:
可以看到确实是第一个任务过了 1S 后执行, 第二个任务在第一个任务执行完后过 3S 执行了.
这就说明了 Timer 只创建唯一的线程来执行所有 Timer 任务. 如果一个 timer 任务的执行很耗时, 会导致其他 TimerTask 的时效准确性出问题.
Timer 实现原理分析
下面简单介绍下 Timer 的原理, 如下图是 Timer 的原理模型介绍:
1. 其中 TaskQueue 是一个平衡二叉树堆实现的优先级队列, 每个 Timer 对象内部有唯一一个 TaskQueue 队列. 用户线程调用 timer 的 schedule 方法就是把 TimerTask 任务添加到 TaskQueue 队列, 在调用 schedule 的方法时候 long delay 参数用来说明该任务延迟多少时间执行.
2.TimerThread 是具体执行任务的线程, 它从 TaskQueue 队列里面获取优先级最小的任务进行执行, 需要注意的是只有执行完了当前的任务才会从队列里面获取下一个任务而不管队列里面是否有已经到了设置的 delay 时间, 一个 Timer 只有一个 TimerThread 线程, 所以可知 Timer 的内部实现是一个多生产者单消费者模型.
从实现模型可以知道要探究上面的问题只需看 TimerThread 的实现就可以了, TimerThread 的 run 方法主要逻辑源码如下:
- public void run() {
- try {
- mainLoop();
- } finally {
- // 有人杀死了这个线程, 表现得好像 Timer 已取消
- synchronized(queue) {
- newTasksMayBeScheduled = false;
- queue.clear(); // 消除过时的引用
- }
- }
- }
- private void mainLoop() {
- while (true) {
- try {
- TimerTask task;
- boolean taskFired;
- // 从队列里面获取任务时候要加锁
- synchronized(queue) {
- ......
- }
- if (taskFired)
- task.run();// 执行任务
- } catch(InterruptedException e) {
- }
- }
- }
可知当任务执行过程中抛出了除 InterruptedException 之外的异常后, 唯一的消费线程就会因为抛出异常而终止, 那么队列里面的其他待执行的任务就会被清除. 所以 TimerTask 的 run 方法内最好使用 try-catch 结构 catch 主可能的异常, 不要把异常抛出到 run 方法外.
其实要实现类似 Timer 的功能使用 ScheduledThreadPoolExecutor 的 schedule 是比较好的选择. ScheduledThreadPoolExecutor 中的一个任务抛出了异常, 其他任务不受影响的.
ScheduledThreadPoolExecutor 例子如下:
- /**
- * Created by cong on 2018/7/12.
- */
- public class ScheduledThreadPoolExecutorTest {
- static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
- public static void main(String[] args) {
- scheduledThreadPoolExecutor.schedule(new Runnable() {
- 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() {
- public void run() {
- for (int i =0;i<5;++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 捕获异常并打印日志是最佳实践.
来源: https://www.cnblogs.com/huangjuncong/p/9296897.html