JDK自带的java.util.Timer定时器的实现原理

简介:

 Timer和TimerTask      Since JDK1.3

Timer中最主要由三个部分组成: 任务 TimerTask 、  任务队列: TaskQueue queue 和 任务调试者:TimerThread thread, 他们之间的关系可以通过下面图示:


      在这个图中,可以清楚地看到这Timer本身及其和这三个部分的关系:

      1. Timer可以看作是面向开发人员的一个"接口"

      2. 所有向Timer添加的任务都会被放入一个TaskQueue类型的任务队列中去.(如何安排任务优先级顺序下文会讲)

      3. 任务调度由TimerThread负责.

 任务单元 TimerTask

 首先看一下任务单元实体类: TimerTask. 在这个类中, 要关注的是任务状态和几个状态常量:


 
 
  1. /** 标识任务的状态 */   
  2. int state = VIRGIN;   
  3. /** 任务的状态的常量 */   
  4. static final int VIRGIN = 0;   
  5. static final int SCHEDULED   = 1;   
  6. static final int EXECUTED    = 2;   
  7.  


 
 
  1. static final int CANCELLED   = 3;   

以及一个比较重要的两个成员变量:


 
 
  1. long nextExecutionTime;   
  2. long period = 0;

nextExecutionTime 这个成员变量用到记录该任务下次执行时间, 其格式和System.currentTimeMillis()一致.这个值是作为任务队列中任务排序的依据. 任务调试者执行每个任务前会对这个值作处理,重新计算下一次任务执行时间,并为这个变量赋值. 

period 用来描述任务的执行方式: 0表示不重复执行的任务. 正数表示固定速率执行的任务. 负数表示固定延迟执行的任务. (固定速率: 不考虑该任务上一次执行情况,始终从开始时间算起的每period执行下一次.   固定延迟: 考虑该任务一次执行情况,在上一次执行后period执行下一次).


任务队列 TaskQueue

    事实上任务队列是一个数组, 采用平衡二叉堆来实现他的优先级调度, 并且是一个小顶堆. 需要注意的是, 这个堆中queue[n]的孩子是queue[2*n] 和 queue[2*n+1].

    任务队列的优先级按照TimerTask类的成员变量nextExecutionTime值来排序(注意, 这里的任务指的是那些交由定时器来执行的, 继承TimerTask的对象).
    在任务队列中, nextExecutionTime最小就是所有任务中最早要被调度来执行的, 所以被安排在queue[1] (假设任务队列非空).
    对于堆中任意一个节点n, 和他的任意子孙节点d,一定遵循: n.nextExecutionTime <= d.nextExecutionTime.

1. 添加任务


 
 
  1. void add(TimerTask task) {   
  2.       if (size + 1 == queue.length)   
  3.       queue = Arrays.copyOf(queue, 2 * queue.length);   
  4.       queue[++size] = task;    
  5.       fixUp(size);   
  6. }  

 

首先会判断是否已经满了,(任务队列的初始容量是128 ),如果已经满了, 那么容量扩大至原来2倍, 然后将需要添加的任务放到队列最后. 之后就会调用fixUp 方法来进行队列中任务优先级调整. fixUp方法的作用是尽量将队列中指定位置(k)的任务向队列前面移动, 即提高它的优先级. 因为新加入的方法很有可能比已经在任务队列中的其它任务要更早执行.


 
 
  1. private void fixUp(int k) {   
  2.     while (k > 1) {   
  3.         int j = k >> 1// 对于正数,右移位 <==> j = k/2, 所以j的位置就是k的父亲节点   
  4.         if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)   
  5.             break;   
  6.         TimerTask tmp = queue[j];   
  7.         queue[j] = queue[k];   
  8.         queue[k] = tmp;   
  9.         k = j;   
  10.     }   
  11. }  

 这个过程可以这个描述: 不断地将k位置上元素和它的父亲进行比较, 上文也提到过了. 由于必须满足 "对于堆中任意一个节点n, 和他的任意子孙节点d,一定遵循: n.nextExecutionTime <= d.nextExecutionTime.", 那么在不断比较过程中, 如果发现孩子节点比父亲小的时候, 那么将父亲和孩子位置互换. 直到来到队列第一个位置.

2. 移除任务


 
 
  1. void removeMin() {   
  2.     queue[1] = queue[size];   
  3.     queue[size--] = null// Drop extra reference to prevent memory leak   
  4.     fixDown(1);   

 

从任务队列中移除一个任务的过程, 首先直接将当前任务队列中最后一个任务赋给queue[1], 然后将队列中任务数量--, 最后和上面类似, 但是这里是调用fixDown(int k)方法了, 尽量将k位置的任务向队列后面移动.


 
 
  1. /**  
  2.  * -将k位置的元素向堆底方向移动.<br>  
  3.  * 1. j = k << 1, 将j定位到儿子中.<br>  
  4.  * 2. 将 j 精确定位到较小的儿子.<br>  
  5.  * 3. 然后k与j比较,如果k大于j的话, 那么互换<br>  
  6.  * 4.继续...  
  7.  */   
  8. private void fixDown(int k) {   
  9.     int j;   
  10.     // 如果还没有到队列的最后,并且没有溢出( j > 0 )   
  11.     // 在没有出现溢出的情况下, j = k << 1 等价于 j = 2 * k ;   
  12.     while ((j = k << 1) <= size && j > 0) {   
  13.         // 找到k的两个孩子中小的那个.   
  14.         if (j < size && queue[j].nextExecutionTime > queue[j + 1].nextExecutionTime)   
  15.             j++; // j indexes smallest kid   
  16.         // 找到这个较小的孩子后,(此时k是父亲,j是较小的儿子),父亲和儿子互换位置,即k和j换位子.这样一直下去就可以将这个较大的queue[1]向下堆底移动了.   
  17.         if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)   
  18.             break;   
  19.         TimerTask tmp = queue[j];   
  20.         queue[j] = queue[k];   
  21.         queue[k] = tmp;   
  22.         k = j;   
  23.     }   
  24. }   

下面来看看任务调度者是如何工作的.

任务调度 TimerThread

关于任务调度主要要讲下一个成员变量 newTasksMayBeScheduled 和 调度方法 mainLoop().


 
 
  1. boolean newTasksMayBeScheduled = true;  


 
 
  1. private void mainLoop() {   
  2.         while (true) {   
  3.             try {   
  4.                 TimerTask task;   
  5.                 boolean taskFired = false;   
  6.                 synchronized (queue) {   
  7.                     while (queue.isEmpty() && newTasksMayBeScheduled) {   
  8.                         queue.wait();   
  9.                     }   
  10.                     if (queue.isEmpty())   
  11.                         break// 直接挑出mainLoop了.   
  12.                     long currentTime, executionTime;   
  13.                     task = queue.getMin(); // 获取这个任务队列第一个任务   
  14.                     synchronized (task.lock) {   
  15.                         if (task.state == TimerTask.CANCELLED) {   
  16.                             queue.removeMin();   
  17.                             continue;   
  18.                         }   
  19.                         currentTime = System.currentTimeMillis();   
  20.                         executionTime = task.nextExecutionTime;   
  21.                         if (taskFired = (executionTime <= currentTime)) {   
  22.                             if (task.period == 0) { // Non-repeating, remove   
  23.                                 queue.removeMin();   
  24.                                 task.state = TimerTask.EXECUTED;   
  25.                             } else { // Repeating task, reschedule   
  26.                                 queue.rescheduleMin(task.period < 0 ? currentTime - task.period : executionTime   
  27.                                         + task.period);   
  28.                             }   
  29.                         }   
  30.                     }//释放锁   
  31.                     if (!taskFired)   
  32.                         queue.wait(executionTime - currentTime);   
  33.                 }   
  34.                 if (taskFired) // Task fired; run it, holding no locks   
  35.                     task.run();   
  36.             } catch (InterruptedException e) {   
  37.             }   
  38.         }// while(true)   
  39.     }   

 

 newTasksMayBeScheduled变量用来表示是否需要继续等待新任务了. 

默认情况这个变量是true , 并且这个变量一直是true的,只有两种情况的时候会变成 false 
  1.当调用Timer的cancel方法
  2.没有引用指向Timer对象了.

任务调度: mainLoop()方法中的一个while可以理解为一次任务调度:

STEP 1 :  判断任务队列中是否还有任务, 如果任务队列为空了, 但是newTasksMayBeScheduled变量还是true, 表明  需要继续等待新任务, 所以一直等待.

STEP 2 : 等待唤醒后, 再次判断队列中是否有任务. 如果还是没有任务,那么直接结束定时器工作了.因为queue只在两个地方被调用: addTask和cancel  1.向任务队列中增加任务会唤醒  2.timer.cancel()的时候也会唤醒. 那么这里如果还是empty,那么就是cancel的唤醒了,所以可以结束timer工作了.

STEP 3 : 从任务队列中取出第一个任务,即nextExecutionTime最小的那个任务.

STEP 4: 判断这个任务是否已经被取消. 如果已经被取消了,那么就直接从任务队列中移除这个任务(removeMin() ),然后直接进入下一个任务调度周期.

STEP 5 : 判断是否到了或者已经超过了这个任务应该执行的时间了.

      如果到了 , 不会立即执行它,而是会在这次循环的最后来执行它.
      这里做的事情可以看作是为下一个调度周期进行准备:包括:
       1. 判断是否是重复(repeating)任务,如果 task.period == 0, 那么就不是重复任务,所以可以直接将这个任务从任务队列中移除了(removeMin() ),因为没有必要留到下一个调度周期中去了.
      2. 如果是需要重复执行的任务, 那么就要重新设置这个任务的nextExecutionTime,即调用方法queue.rescheduleMin(long) ,这个方法中会调用fixDown(1) 负责重新调整任务队列的优先级顺序.

      如果还没有到执行时间 ,  一直等到 queue.wait(executionTime - currentTime)

并且等待完毕后,似乎可以开始运行了, 但是这里设计成不立即运行,而是直接进入下一个任务调度周期.(因为taskFired =false,所以不会在这次进行执行的.)       

STEP: 6 开始调用任务的run方法运行任务.




本文转自 nileader 51CTO博客,原文链接:http://blog.51cto.com/nileader/392343,如需转载请自行联系原作者

相关文章
|
7月前
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
370 17
Java HashMap详解及实现原理
|
12月前
|
Java Linux
java基础(3)安装好JDK后使用javac.exe编译java文件、java.exe运行编译好的类
本文介绍了如何在安装JDK后使用`javac.exe`编译Java文件,以及使用`java.exe`运行编译好的类文件。涵盖了JDK的安装、环境变量配置、编写Java程序、使用命令行编译和运行程序的步骤,并提供了解决中文乱码的方法。
391 2
|
存储 算法 Java
【Java集合类面试八】、 介绍一下HashMap底层的实现原理
HashMap基于hash算法,通过put和get方法存储和获取对象,自动调整容量,并在碰撞时用链表或红黑树组织元素以优化性能。
|
10月前
|
安全 Java 编译器
JDK 10中的局部变量类型推断:Java编程的简化与革新
JDK 10引入的局部变量类型推断通过`var`关键字简化了代码编写,提高了可读性。编译器根据初始化表达式自动推断变量类型,减少了冗长的类型声明。虽然带来了诸多优点,但也有一些限制,如只能用于局部变量声明,并需立即初始化。这一特性使Java更接近动态类型语言,增强了灵活性和易用性。
198 53
|
9月前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
12月前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
11月前
|
Java
让星星⭐月亮告诉你,jdk1.8 Java函数式编程示例:Lambda函数/方法引用/4种内建函数式接口(功能性-/消费型/供给型/断言型)
本示例展示了Java中函数式接口的使用,包括自定义和内置的函数式接口。通过方法引用,实现对字符串操作如转换大写、数值转换等,并演示了Function、Consumer、Supplier及Predicate四种主要内置函数式接口的应用。
91 1
|
12月前
|
Oracle Java 关系型数据库
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决
如果遇到"exec format error"问题,文章建议先检查Linux操作系统是32位还是64位,并确保安装了与系统匹配的JDK版本。如果系统是64位的,但出现了错误,可能是因为下载了错误的JDK版本。文章提供了一个链接,指向Oracle官网上的JDK 17 Linux版本下载页面,并附有截图说明。
Linux下JDK环境的配置及 bash: /usr/local/java/bin/java: cannot execute binary file: exec format error问题的解决
|
安全 Java API
【性能与安全的双重飞跃】JDK 22外部函数与内存API:JNI的继任者,引领Java新潮流!
【9月更文挑战第7天】JDK 22外部函数与内存API的发布,标志着Java在性能与安全性方面实现了双重飞跃。作为JNI的继任者,这一新特性不仅简化了Java与本地代码的交互过程,还提升了程序的性能和安全性。我们有理由相信,在外部函数与内存API的引领下,Java将开启一个全新的编程时代,为开发者们带来更加高效、更加安全的编程体验。让我们共同期待Java在未来的辉煌成就!
234 11
|
监控 Java 大数据
【Java内存管理新突破】JDK 22:细粒度内存管理API,精准控制每一块内存!
【9月更文挑战第9天】虽然目前JDK 22的确切内容尚未公布,但我们可以根据Java语言的发展趋势和社区的需求,预测细粒度内存管理API可能成为未来Java内存管理领域的新突破。这套API将为开发者提供前所未有的内存控制能力,助力Java应用在更多领域发挥更大作用。我们期待JDK 22的发布,期待Java语言在内存管理领域的持续创新和发展。