概述

在Java开发中,用过定时功能的同学一定不会对Timer感到陌生。不过,除了Timer,在Java 5之后又引入了一个定时工具ScheduledThreadPoolExecutor,那么我们应该如何在这两个定时工具之间进行选择呢?

一般情况下我们都建议使用ScheduledThreadPoolExecutor而不是Timer,主要原因有以下3点:

  1. Timer使用的是绝对时间,系统时间的改变会对Timer产生一定的影响;而ScheduledThreadPoolExecutor使用的是相对时间,所以不会有这个问题。
  2. Timer使用单线程来处理任务,长时间运行的任务会导致其他任务的延时处理,而ScheduledThreadPoolExecutor可以自定义线程数量。
  3. Timer没有对运行时异常进行处理,一旦某个任务触发运行时异常,会导致整个Timer崩溃,而ScheduledThreadPoolExecutor对运行时异常做了捕获(可以在afterExecute()回调方法中进行处理),所以更加安全。

下面我们就来通过了解Timer与ScheduledThreadPoolExecutor的运行原理来理解上面几个问题出现的原因。

Timer的运行机制

  • TimerTask:任务类。内部持有nextExecutionTime变量,表示任务实际执行时间点,单位为毫秒,使用System.currentTimeMillis() + delay计算得出。
  • TimerQueue:使用小根堆实现的优先队列。按照TimerTask的实际执行时间点由小到大排序。
  • TimerThread:顾名思义,这是实际执行任务的线程。

TimerThread会在Timer初始化后启动,之后会进入mainLoop()方法,该方法会不断从TimerQueue中取出时间点最小的TimerTask。如果该TimerTask的执行时间点已到,则直接调用TimerTask.run()执行;否则,调用wait()方法,等待相应的时间。

而我们调用Timer.schedule()方法,实际上是通过TimerQueue.add()方法,将TimerTask加入任务等待队列。

这里还有一个需要注意的地方是:当加入任务的执行时间点是优先队列中最小的时,就调用notify()方法唤醒TimerThread,而TimerThread在被唤醒后会重新调用TimerQueue.getMin()方法,再次调用wait(),不过这次的等待时间就变成了新加入任务的时间点。

ScheduledThreadPoolExecutor的运行机制

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,对线程池的原理不了解的同学,可以看一下我的这篇文章:从零实现ImageLoader(三)—— 线程池详解。

ScheduledThreadPoolExecutor的实现比Timer要复杂一些,不过要是理解了线程池的运行原理,其实也不难。它只不过是在ThreadPoolExecutor的基础上使用自定义的阻塞队列DelayedWorkQueue来实现任务定时功能。所以ScheduledThreadPoolExecutor的运行流程其实和ThreadPoolExecutor是差不多的。

  • ScheduledFutureTask:任务类。内部持有time变量,单位为纳秒,通过System.nanoTime() + delay计算得出。
  • DelayedWorkQueue:使用小根堆实现的优先阻塞队列,将ScheduledFutureTask按照从小到大的顺序排列,同时在take()方法内实现阻塞操作。
  • WorkerThread:这里为了简单起见,我将线程池的核心线程和临时线程统一写成WorkerThread,但需要注意的是ScheduledThreadPoolExecutor是线程池的一个子类,所以线程池的那一套东西在ScheduledThreadPoolExecutor里也是有的。

光从这两个图上看,好像ScheduledThreadPoolExecutor和Timer的实现都大同小异,不过是换了一些名字,但实际上这两个的实现还是有很大的不同的,不止因为ScheduledThreadPoolExecutor使用的是多线程。

在Timer里定时功能的实现主要依靠TimerThread.mainLoop()的等待,而ScheduledThreadPoolExecutor使用的是多线程,在每个线程里都单独实现定时功能是不现实的,因此,ScheduledThreadPoolExecutor将定时功能放在了DelayedWorkQueue类里,而由于DelayedWorkQueue是阻塞队列,所以定时任务的实现实际上就在DelayedWorkQueue.take()方法中。下面我们就来分析一下DelayedWorkQueue.take()到底做了什么。

Leader/Follower模式

在多线程网络编程中,我们一般使用一个线程监听端口,在接收到事件后再使用其他的线程去完成操作。这种情况下,在两个线程之间的上下文切换开销其实是很大的,于是我们有了Leader/Follower模式:

在Leader/Follower模式中,不存在一个专门用来监听的线程,所有的线程都是等价的,而这些线程会不断在Leader、Follower和Processor这三个状态之间来回切换。

在程序中会保证每个时刻有且只有一个Leader,这个Leader就暂时充当了之前用来监听端口线程的作用。而当有一个新的事件发生时,Leader不再是重新找一个线程去处理连接,而是自己转化为Processor处理事件,并且重新指定一个Follower作为新的Leader。当事件处理完毕后,Processor又会转化为Follower等待重新成为Leader。

take()方法的原理

这里的take()方法就借助了Leader/Follower模式的思想,同一时刻只有一个Leader线程,不过这里由于任务执行的时间点是已经确定了的,所以不再是等待一个触发事件,而是等待最小任务所对应的延迟时间。其他的Follower线程则处于无限等待的状态,直到当前Leader到达指定时间后转化为Processor去处理任务,这时就会唤醒一个Follower作为下一任的Leader。而Processor在处理完任务后又会重新加入Follower进行等待。

绝对时间与相对时间

了解了Timer与ScheduledThreadPoolExecutor的运行机制,下面我们就来看一下Timer的这些缺陷究竟是怎么回事。

首先是绝对时间与相对时间的问题,可能有人已经发现,不管是TimerTask还是ScheduledFutureTask都是存储的实际执行时间点,只不过一个是毫秒,一个是纳秒,难道时间单位还会对这些有影响?确实,时间单位是不会对任务的执行有影响的,不过这里的玄机就在于这个时间的计算方式:System.currentTimeMillis()System.nanoTime()

System.currentTimeMillis()大家已经很清楚了,就是当前时间与1970年1月1日午夜的时间差的毫秒数,而System.nanoTime()又是什么呢?官方文档里是这么说的:

此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数。

这就是Timer与ScheduledThreadPoolExecutor一个是基于绝对时间而另一个是基于相对时间的原因。下面我们写个例子来测试一下:

public static void main(String[] args) {System.out.println("Start:\t" + new Date());Executors.newSingleThreadScheduledExecutor().schedule(() -> {System.out.println("Executor:\t" + new Date());}, 60, TimeUnit.SECONDS);new Timer().schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Timer:\t" + new Date());}}, 60000);
}复制代码

输出:

Start:    Sun Oct 08 10:51:44 CST 2017
Executor:    Sun Oct 08 10:51:41 CST 2017
Timer:    Sun Oct 08 10:52:45 CST 2017复制代码

这里,我在启动之后将系统的时钟向后调了一分钟,所以实际的启动时间应该是10:50:44,由于ScheduledThreadPoolExecutor的等待时间与系统无关,所以在一分钟后执行;而Timer是基于绝对时间的所以在10:52:45执行,实际上这时已经过去两分钟了。

单线程与多线程

Timer的第二个缺陷是,由于它使用的是单线程,所以长时间执行的任务会对其他任务产生影响。

public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());ScheduledExecutorService service = Executors.newScheduledThreadPool(3);service.schedule(() -> {System.out.println("Executor 任务1:\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}, 60, TimeUnit.SECONDS);service.schedule(() -> {System.out.println("Executor 任务2:\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}, 60, TimeUnit.SECONDS);Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Timer 任务1:\t\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}}, 60000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Timer 任务2:\t\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}}, 60000);
}复制代码

输出:

Start:            Sun Oct 08 11:10:34 CST 2017
Executor 任务1:    Sun Oct 08 11:11:34 CST 2017
Executor 任务2:    Sun Oct 08 11:11:34 CST 2017
Timer 任务1:        Sun Oct 08 11:11:34 CST 2017
Timer 任务2:        Sun Oct 08 11:12:04 CST 2017复制代码

可以看到ScheduledThreadPoolExecutor中的两个任务在等待一分钟之后同时执行;而在Timer中的任务2却因任务1长达半分钟的执行时间,总共等了一分半钟才得以执行。

异常处理

最后我们来看一下Timer与ScheduledThreadPoolExecutor对异常的处理情况:

Timer

Timer内部没有对异常做任何处理,如果任务执行发生运行时异常,整个TimerThread都会崩溃:

public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {throw new RuntimeException("Timer 任务1");}}, 60000);timer.schedule(new TimerTask() {@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Timer 任务2:\t\t" + new Date());}}, 60000);
}复制代码

输出:

Start:            Sun Oct 08 11:53:05 CST 2017
Exception in thread "Timer-0" java.lang.RuntimeException: Timer 任务1at main.Main$1.run(Main.java:32)at java.util.TimerThread.mainLoop(Timer.java:555)at java.util.TimerThread.run(Timer.java:505)复制代码

可以看到,任务1抛出的运行时异常导致整个Timer线程崩溃,任务2自然也没有执行。

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor中对异常的处理实际上是ThreadPoolExecutor类完成的,ThreadPoolExecutor在任务运行时对异常做了捕获,并且将异常传入了afterExecute()方法:

public class ThreadPoolExecutor extends AbstractExecutorService {final void runWorker(Worker w) {...Throwable thrown = null;try {task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}...}
}复制代码

我们来验证一下:

public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();service.schedule(() -> {throw new RuntimeException("Executor 任务1");}, 60, TimeUnit.SECONDS);service.schedule(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Executor 任务2:\t" + new Date());}, 60, TimeUnit.SECONDS);
}复制代码

输出:

Start:            Sun Oct 08 11:33:35 CST 2017
Executor 任务2:    Sun Oct 08 11:34:36 CST 2017复制代码

可以看到这里虽然任务1抛出了运行时异常,但由于线程池内部完善的异常处理机制,任务2得以成功执行。

后记

看了这么多Timer的缺陷,你还在犹豫吗?赶快放弃Timer,投入ScheduledThreadPoolExecutor的怀抱吧!

为什么你不该用Timer相关推荐

  1. Timer定时器开发

    Timer定时器开发 定时器的作用是不占线程的等待一个确定时间,同样通过callback来通知定时器到期. 参考:https://github.com/sogou/workflow 定时器的创建 同样 ...

  2. Timer 的简单介绍

    1 Timer timer=new Timer(); (个人建议使用的时候不要直接就new一个,原因是,还没使用呢就先分配一个空间, 我们使用private Timer timer: 然后在使用前  ...

  3. nordic 51822 sdk. timer 的使用

    它的源代码和头文件分别为app_timer.c/app_timer.h.这是Nordic为我们提供的虚拟定时器,这个定时器不同于硬件上的TIMER,而是基于RTC1实现的一种虚拟定时器,其将定时功能作 ...

  4. Standup Timer的MVC模式及项目结构分析

    前言 学习android一段时间了,为了进一步了解android的应用是如何设计开发的,决定详细研究几个开源的android应用.从一些开源应用中吸收点东西,一边进行量的积累,一边探索android的 ...

  5. C# Timer使用方法示例

    实例化一个timer: // 每5分钟执行一次,每次执行的间隔毫秒时长 System.Timers.Timer timer = new System.Timers.Timer(5*60*1000); ...

  6. C#中Timer组件用法

    Timer组件是也是一个WinForm组件了,和其他的WinForm组件的最大区别是:Timer组件是不可见的,而其他大部分的组件都是都是可见的,可以设计的.Timer组件也被封装在名称空间Syste ...

  7. Android Timer的使用

    1:服务端使用PHP <?phpecho date('Y-m-d H:i:s'); ?> 2:activity_main.xml <RelativeLayout xmlns:andr ...

  8. Linux内核中关于定时器Timer的应用

    2019独角兽企业重金招聘Python工程师标准>>> 在Touchscreen驱动中 1 声明  Ad7877.c (\linux-2.6.30.4\drivers\input\t ...

  9. silverlight、wpf中 dispatcher和timer区别

    相同点:都是定时执行任务的计时器,都可以使用. 不同点:Timer运行在非UI 线程,如果Timer需要更新UI的时候,需要调用 Invoke或者 BeginInvoke DispatcherTime ...

  10. java timer cron_Java之旅--定时任务(Timer、Quartz、Spring、LinuxCron)

    在Java中,实现定时任务有多种方式.本文介绍4种.Timer和TimerTask.Spring.QuartZ.Linux Cron. 以上4种实现定时任务的方式.Timer是最简单的.不须要不论什么 ...

最新文章

  1. android bitmap 占用内存大小,drawable与bitmap内存占用大小
  2. VC6开发视频监控ActiveX控件总结
  3. ArcGIS实验教程——实验十九:网络分析(最短路径实现)
  4. servlet乱码 解决方法 2种方法
  5. jmeter constant timer 如何添加_JMeter性能测试入门篇
  6. php排列组合1004无标题,PHP的排列组合有关问题
  7. 21 年前濒临倒闭的苹果是如何做到今天万亿市值的?
  8. hive:导出数据记录中null被替换为\n的解决方案
  9. (二)匈牙利算法简介
  10. numpy库学习总结
  11. 后RCNN时代的物体检测及实例分割进展
  12. 29、java中阻塞队列
  13. 从Logistic Regression 到 Neural Network
  14. java实现lda模型_lda模型 java
  15. 中文版ASAM OpenSCENARIO与OpenDRIVE标准正式发布
  16. 分享两个在线制图网站
  17. C++编程基础(1)-C中的malloc/free和C++中的new/delete
  18. 【Flink实战系列】Flink 1.11.1 on yarn 集群搭建教程
  19. FPGA开发技巧备忘录——目录
  20. IT行业的几大发展趋势

热门文章

  1. python的沙盒环境virtualenv(一)--作用
  2. 安装Adventure Works 2008 R2演示数据库
  3. 如何配置和部署安全的.NET三层应用
  4. Quartus使用技巧(一些常用的方法)
  5. ZOJ 3609 Modular Inverse(扩展欧几里得)题解
  6. Python运算符及注释
  7. Oracle_052_lesson_p9
  8. 第130天:移动端-rem布局
  9. 我使用的PhpStorm_已迁移
  10. [翻译] Shimmer