• java的Timer全网最详细总结


    1.简介

    在Java 1.3版本中引入了Timer工具类,它是一个古老的定时器,通常与TimerTaskTaskQueue一起使用。Timer工具类的实现涉及到TimerTask类、Timer类、TimerQueue类和TimerThread类。其中,TimerQueueTimerThread类与Timer类位于同一个类文件中,由Timer类内部调用。

    然而,从Java 5开始,在并发包中引入了另一个定时器实现:ScheduledThreadPoolExecutorScheduledThreadPoolExecutorTimer做了许多改进,并提供了更多的功能和工具,可以看作是对Timer的改进和替代。

    为什么还要介绍Timer工具类呢?通过了解Timer的功能和它背后的原理,我们可以更好地对比和理解ScheduledThreadPoolExecutor。同时,ScheduledThreadPoolExecutor的一些改进思想在我们平时的编码工作中也可以借鉴。

    先画上一张图,描述一下Timer的大致模型,Timer的模型很容易理解,即任务加入到任务队列中,由任务处理线程循环从任务队列取出任务执行

    2.主要成员变量

    Timer中用到的主要是两个成员变量:

    1. TaskQueue:一个按照时间优先排序的队列,这里的时间是每个定时任务下一次执行的毫秒数(相对于1970年1月1日而言)
    2. TimerThread:对TaskQueue里面的定时任务进行编排和触发执行,它是一个内部无限循环的线程。
    1. //根据时间进行优先排序的队列
    2. private final TaskQueue queue = new TaskQueue();
    3. //消费线程,对queue中的定时任务进行编排和执行
    4. private final TimerThread thread = new TimerThread(queue);
    5. //构造函数
    6. public Timer(String name) {
    7. thread.setName(name);
    8. thread.start();
    9. }

    3.定时功能

    Timer提供了三种定时模式:

    1. 一次性任务
    2. 按照固定的延迟执行(fixed delay)
    3. 按照固定的周期执行(fixed rate)

    第一种比较好理解,即任务只执行一次;针对第一种,Timer提供了以下两个方法:

    1. //在当前时间往后delay个毫秒开始执行
    2. public void schedule(TimerTask task, long delay) {...}
    3. //在指定的time时间点执行
    4. public void schedule(TimerTask task, Date time) {...}

    第二种Fixed Delay模式也提供了以下两个方法

    1. //从当前时间开始delay个毫秒数开始定期执行,周期是period个毫秒数
    2. public void schedule(TimerTask task, long delay, long period) {...}
    3. 从指定的firstTime开始定期执行,往后每次执行的周期是period个毫秒数
    4. public void schedule(TimerTask task, Date firstTime, long period){...}

    它的工作方式是:

    第一次执行的时间将按照指定的时间点执行(如果此时TimerThread不在执行其他任务),如有其他任务在执行,那就需要等到其他任务执行完成才能执行。

    从第二次开始,每次任务的执行时间是上一次任务开始执行的时间加上指定的period毫秒数。

    如何理解呢,我们还是看代码

    1. public static void main(String[] args) {
    2. TimerTask task1 = new DemoTimerTask("Task1");
    3. TimerTask task2 = new DemoTimerTask("Task2");
    4. Timer timer = new Timer();
    5. timer.schedule(task1, 1000, 5000);
    6. timer.schedule(task2, 1000, 5000);
    7. }
    8. static class DemoTimerTask extends TimerTask {
    9. private String taskName;
    10. private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
    11. public DemoTimerTask(String taskName) {
    12. this.taskName = taskName;
    13. }
    14. @Override
    15. public void run() {
    16. System.out.println(df.format(new Date()) + taskName + " is working.");
    17. try {
    18. Thread.sleep(2000);
    19. } catch (InterruptedException e) {
    20. // TODO Auto-generated catch block
    21. e.printStackTrace();
    22. }
    23. System.out.println(df.format(new Date()) + taskName + " finished work.");
    24. }
    25. }

    task1和task2是几乎同时执行的两个任务,而且执行时长都是2秒钟,如果此时我们把第六行注掉不执行,我们将得到如下结果(和第三种Fixed Rate模式结果相同):

    1. 13:42:58---Task1 is working.
    2. 13:43:00---Task1 finished work.
    3. 13:43:03---Task1 is working.
    4. 13:43:05---Task1 finished work.
    5. 13:43:08---Task1 is working.
    6. 13:43:10---Task1 finished work.

    如果打开第六行,我们再看下两个任务的执行情况。我们是期望两个任务能够同时执行,但是Task2是在Task1执行完成后才开始执行(原因是TimerThread是单线程的,每个定时任务的执行也在该线程内完成,当多个任务同时需要执行时,只能是阻塞了),从而导致Task2第二次执行的时间是它上一次执行的时间(13:43:57)加上5秒钟(13:44:02)。

    1. 13:43:55---Task1 is working.
    2. 13:43:57---Task1 finished work.
    3. 13:43:57---Task2 is working.
    4. 13:43:59---Task2 finished work.
    5. 13:44:00---Task1 is working.
    6. 13:44:02---Task1 finished work.
    7. 13:44:02---Task2 is working.
    8. 13:44:04---Task2 finished work.

    那如果此时还有个Task3也是同样的时间点和间隔执行会怎么样呢?

    结论是:也将依次排队,执行的时间依赖两个因素:

    1.上次执行的时间

    2.期望执行的时间点上有没有其他任务在执行,有则只能排队了


    我们接下来看下第三种Fixed Rate模式,我们将上面的代码稍作修改:

    1. public static void main(String[] args) {
    2. TimerTask task1 = new DemoTimerTask("Task1");
    3. TimerTask task2 = new DemoTimerTask("Task2");
    4. Timer timer = new Timer();
    5. timer.scheduleAtFixedRate(task1, 1000, 5000);
    6. timer.scheduleAtFixedRate(task2, 1000, 5000);
    7. }
    8. static class DemoTimerTask extends TimerTask {
    9. private String taskName;
    10. private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
    11. public DemoTimerTask(String taskName) {
    12. this.taskName = taskName;
    13. }
    14. @Override
    15. public void run() {
    16. System.out.println(df.format(new Date()) + taskName + " is working.");
    17. try {
    18. Thread.sleep(2000);
    19. } catch (InterruptedException e) {
    20. // TODO Auto-generated catch block
    21. e.printStackTrace();
    22. }
    23. System.out.println(df.format(new Date()) + taskName + " finished work.");
    24. }
    25. }

    Task1和Task2还是在相同的时间点,按照相同的周期定时执行任务,我们期望Task1能够每5秒定时执行任务,期望的时间点是:14:21:47-14:21:52-14:21:57-14:22:02-14:22:07,实际上它能够交替着定期执行,原因是Task2也会定期执行,并且对TaskQueue的锁他们是交替着拿的(这个在下面分析TimerThread源码的时候会讲到)

    1. 14:21:47---Task1 is working.
    2. 14:21:49---Task1 finished work.
    3. 14:21:49---Task2 is working.
    4. 14:21:51---Task2 finished work.
    5. 14:21:52---Task2 is working.
    6. 14:21:54---Task2 finished work.
    7. 14:21:54---Task1 is working.
    8. 14:21:56---Task1 finished work.
    9. 14:21:57---Task1 is working.
    10. 14:21:59---Task1 finished work.
    11. 14:21:59---Task2 is working.
    12. 14:22:01---Task2 finished work.

    4.TimerThread

    上面我们主要讲了Timer的一些主要源码及定时模式,下面我们来分析下支撑Timer的定时任务线程TimerThread。

    TimerThread大概流程图如下:

    TimerThread流程

    源码解释如下:

    1. private void mainLoop() {
    2. while (true) {
    3. try {
    4. TimerTask task;
    5. boolean taskFired;
    6. synchronized(queue) {
    7. // 如果queue里面没有要执行的任务,则挂起TimerThread线程
    8. while (queue.isEmpty() && newTasksMayBeScheduled)
    9. queue.wait();
    10. // 如果TimerThread被激活,queue里面还是没有任务,则介绍该线程的无限循环,不再接受新任务
    11. if (queue.isEmpty())
    12. break;
    13. long currentTime, executionTime;
    14. // 获取queue队列里面下一个要执行的任务(根据时间排序,也就是接下来最近要执行的任务)
    15. task = queue.getMin();
    16. synchronized(task.lock) {
    17. if (task.state == TimerTask.CANCELLED) {
    18. queue.removeMin();
    19. continue; // No action required, poll queue again
    20. }
    21. currentTime = System.currentTimeMillis();
    22. executionTime = task.nextExecutionTime;
    23. // taskFired表示是否需要立刻执行线程,当task的下次执行时间到达当前时间点时为true
    24. if (taskFired = (executionTime<=currentTime)) {
    25. //task.period==0表示这个任务只需要执行一次,这里就从queue里面删掉了
    26. if (task.period == 0) {
    27. queue.removeMin();
    28. task.state = TimerTask.EXECUTED;
    29. } else { // Repeating task, reschedule
    30. //针对task.period不等于0的任务,则计算它的下次执行时间点
    31. //task.period<0表示是fixed delay模式的任务
    32. //task.period>0表示是fixed rate模式的任务
    33. queue.rescheduleMin(
    34. task.period<0 ? currentTime - task.period
    35. : executionTime + task.period);
    36. }
    37. }
    38. }
    39. // 如果任务的下次执行时间还没有到达,则挂起TimerThread线程executionTime - currentTime毫秒数,到达执行时间点再自动激活
    40. if (!taskFired)
    41. queue.wait(executionTime - currentTime);
    42. }
    43. // 如果任务的下次执行时间到了,则执行任务
    44. // 注意:这里任务执行没有另起线程,还是在TimerThread线程执行的,所以当有任务在同时执行时会出现阻塞
    45. if (taskFired)
    46. // 这里没有try catch异常,当TimerTask抛出异常会导致整个TimerThread跳出循环,从而导致Timer失效
    47. task.run();
    48. } catch(InterruptedException e) {
    49. }
    50. }
    51. }

    5.TaskQueue

    TaskQueue是Timer类文件中封装的一个队列数据结构,内部默认是一个长度128的TimerTask数组,当任务加入时,检测到数组将满将会自动扩容1倍,并对数组元素根据下次执行时间nextExecutionTime按时间从近到远进行排序。 

    1. ```java
    2. void add(TimerTask task) {
    3.     // 检测数组长度,若不够则进行扩容
    4.     if (size + 1 == queue.length)
    5.         queue = Arrays.copyOf(queue, 2*queue.length);
    6. // 任务入队
    7.     queue[++size] = task;
    8. // 排序
    9.     fixUp(size);
    10. }
    11. ```
    12. fixUp方法实现:
    13. ```java
    14. private void fixUp(int k) {
    15.     while (k > 1) {
    16.         int j = k >> 1;
    17.         if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
    18.             break;
    19.         TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
    20.         k = j;
    21.     }
    22. }
    23. ```
    24. TaskQueue中除了fixUp方法外还有一个fixDown方法,这两个其实就是堆排序算法,在算法专题中再进行详细介绍,只要记住他们的任务就是按时间从近到远进行排序,最近的任务排在队首即可。
    25. ```java
    26. private void fixDown(int k) {
    27.     int j;
    28.     while ((j = k << 1) <= size && j > 0) {
    29.         if (j < size &&
    30.             queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
    31.             j++; // j indexes smallest kid
    32.         if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
    33.             break;
    34.         TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
    35.         k = j;
    36.     }
    37. }
    38. void heapify() {
    39.     for (int i = size/2; i >= 1; i--)
    40.         fixDown(i);
    41. }
    42. ```

    6.结论

    通过上面的分析,我们可以得出以下结论:

    1. Timer支持三种模式的定时任务(一次性任务,Fixed Delay模式,Fixed Rate模式)
    2. Timer中的TimerThread是单线程模式,因此导致所有定时任务不能同时执行,可能会出现延迟
    3. TimerThread中并没有处理好任务的异常,因此每个TimerTask的实现必须自己try catch防止异常抛出,导致Timer整体失效
  • 相关阅读:
    记一次失败的pip使用经历
    cmake笔记
    每日一个C库函数-#2-memmove()
    【云原生网关】Kong 使用详解
    同一页面实现recycleView三种布局【recycleView + adapter】
    vue第三版
    HTML5期末考核大作业,网站——旅游景点。 学生旅行 游玩 主题住宿网页
    CH342芯片应用—硬件设计指南
    机器学习之IV编码,分箱&WOE编码
    网络安全在2024好入行吗?
  • 原文地址:https://blog.csdn.net/m0_52226803/article/details/133840651