一、Timer概述

Timer是JDK自带的定时任务调度器,原理是用一个小根堆实现的队列来存放任务,再用一个线程来不断从队列头获取任务。由于队列是按照任务的下次执行时间排好序的,从队列头部获取的任务就是最近该执行的任务项。

public class Timer {//任务队列private final TaskQueue queue = new TaskQueue();//执行任务的线程private final TimerThread thread = new TimerThread(queue);
}

二、 TaskQueue类

TaskQueue 是一个基于堆排序的有序队列。

class TaskQueue {//用数组来保存任务项private TimerTask[] queue = new TimerTask[128];//队列中任务的个数private int size = 0;
}

在增加、删除操作时使用向上调整、向下调整等堆排序基本操作对任务数组进行排序,排序依据是任务的下次执行时间(即TimerTask的nextExecutionTime字段)。

add方法源码如下:

   void add(TimerTask task) {// Grow backing store if necessaryif (size + 1 == queue.length)queue = Arrays.copyOf(queue, 2*queue.length);queue[++size] = task;fixUp(size);}private void fixUp(int k) {while (k > 1) {int j = k >> 1;if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)break;TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;k = j;}}

removeMin方法源码如下:

    void removeMin() {queue[1] = queue[size];queue[size--] = null;  // Drop extra reference to prevent memory leakfixDown(1);}private void fixDown(int k) {int j;while ((j = k << 1) <= size && j > 0) {if (j < size &&queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)j++; // j indexes smallest kidif (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)break;TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;k = j;}}

三、 TimerThread类

任务执行线程TimerThread继承Thread类,实现了run方法,同时持有任务队列的引用。

class TimerThread extends Thread {boolean newTasksMayBeScheduled = true;private TaskQueue queue;TimerThread(TaskQueue queue) {this.queue = queue;}public void run() {try {mainLoop();} finally {// Someone killed this Thread, behave as if Timer cancelledsynchronized(queue) {newTasksMayBeScheduled = false;queue.clear();  // Eliminate obsolete references}}}
}

在run方法中,我们看到它直接调用了mainLoop()方法,mainLoop方法才是真正的执行逻辑。

private void mainLoop() {while (true) {try {TimerTask task;boolean taskFired;synchronized(queue) {// 任务队里为空,线程阻塞while (queue.isEmpty() && newTasksMayBeScheduled)queue.wait();if (queue.isEmpty())break; // Queue is empty and will forever remain; die//任务队列不为空,获取第一个任务进行执行long currentTime, executionTime;task = queue.getMin();synchronized(task.lock) {//如果任务状态是已取消,移除任务,从任务继续获取下一个if (task.state == TimerTask.CANCELLED) {queue.removeMin();continue;  // No action required, poll queue again}// 设置当前时间和任务的下次执行时间currentTime = System.currentTimeMillis();executionTime = task.nextExecutionTime;//taskFired任务是否激活,激活条件是任务的下次执行时间早于或等于当前时间if (taskFired = (executionTime<=currentTime)) {//如果任务是一次性的,从队列中删除if (task.period == 0) { // Non-repeating, removequeue.removeMin();task.state = TimerTask.EXECUTED;} else { // Repeating task, reschedule//重复任务需要重新进入队列排序queue.rescheduleMin(task.period<0 ? currentTime   - task.period: executionTime + task.period);}}}if (!taskFired) // Task hasn't yet fired; waitqueue.wait(executionTime - currentTime);}if (taskFired)  // 任务激活,执行任务task.run();} catch(InterruptedException e) {}}}

mainLoop()方法的执行逻辑大体如下:

  1. 判断任务队列是否为空,空则阻塞,直到不空时被唤醒;
  2. 任务队列不为空,获取第一个任务项(即队列头的任务项,因为已经按下次执行时间排好序了),判断任务状态,如果任务是已取消状态,从队列中将其删除,接着跳转到第1步操作;任务状态正常,进行第3步操作
  3. 获取任务的下次执行时间,与当前时间比较:如果大于当前时间,任务还没到时间,线程阻塞X时间(X时间=executionTime - currentTime);如果早于或等于当前时间,任务激活需要被执行,进入第4步操作
  4. 判断任务状态,一次性任务直接从队列删除即可;重复性任务需要重新设定下次执行时间,然后放入队列。
  5. 执行自定义任务的逻辑
  6. 重复第1步

四、schedule方法和scheduleAtFixedRate方法

Timer类的使用主要包含schedule()和scheduleAtFixedRate()两个方法,都能执行重复性任务。区别在于前者已固定的延迟来重复执行,后者以固定的频率来执行。有点拗口,"Show me your code" !

测试1、以固定延迟2秒、固定频率2秒来执行一个执行时长1秒的任务

创建任务类MyTimeTask,内部休眠1秒,打印输出:

class MyTimeTask extends TimerTask {int id ;public MyTimeTask(int id) {this.id = id;}@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}log.info("task excute");}
}

编写schedule测试方法:

    @Testpublic void scheduleTest(){Timer timer = new Timer("myTimerTest",true);MyTimeTask task = new MyTimeTask(10);timer.schedule(task,3000,2000);Scanner scanner = new Scanner(System.in);String operation = scanner.nextLine();if("Q".equalsIgnoreCase(operation)){System.exit(1);}}

结果如下:

23:52:50.722 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
23:52:52.722 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
23:52:54.722 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
23:52:56.723 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute

修改测试方法,使用scheduleAtFixedRate方法调度:

    @Testpublic void scheduleTest(){Timer timer = new Timer("myTimerTest",true);MyTimeTask task = new MyTimeTask(10);timer.scheduleAtFixedRate(task,3000,2000);Scanner scanner = new Scanner(System.in);String operation = scanner.nextLine();if("Q".equalsIgnoreCase(operation)){System.exit(1);}}

结果如下:

00:07:20.724 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:07:22.716 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:07:24.716 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:07:26.716 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute

可以看出结果无差别,都是两秒执行一次任务输出结果。

测试2、以固定延迟2秒、固定频率2秒来执行一个执行时长5秒的任务

修改MyTimeTask的内部休眠为5秒,打印输出:

class MyTimeTask extends TimerTask {/*...省略部分代码...*/@Overridepublic void run() {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}log.info("task excute");}
}

编写schedule测试方法,同上保持不变。

结果如下:

00:20:26.602 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:20:31.607 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:20:36.607 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:20:41.607 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute

修改测试方法,使用scheduleAtFixedRate方法调度,代码测试1中保持不变:

结果如下:

00:22:31.554 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:22:36.562 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:22:41.562 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute
00:22:46.563 [myTimerTest] INFO  c.henu.lab.java.util.TaskQueueTest01 - task excute

可以看出结果无差别,都是5秒执行一次任务输出结果。

好尴尬,两次测试两个方法的结果都没有区别。那是不是两个方法本来就没区别呢?直觉告诉我,JDK老大应该不会这么二,那区别在哪儿呢?

老办法,也是最靠谱的方法,啃源码。

schedule、scheduleAtFixedRate多个重载方法代码如下:

public void schedule(TimerTask task, long delay) {if (delay < 0)throw new IllegalArgumentException("Negative delay.");sched(task, System.currentTimeMillis()+delay, 0);
}public void schedule(TimerTask task, Date time) {sched(task, time.getTime(), 0);
}public void schedule(TimerTask task, long delay, long period) {if (delay < 0)throw new IllegalArgumentException("Negative delay.");if (period <= 0)throw new IllegalArgumentException("Non-positive period.");sched(task, System.currentTimeMillis()+delay, -period);
}public void schedule(TimerTask task, Date firstTime, long period) {if (period <= 0)throw new IllegalArgumentException("Non-positive period.");sched(task, firstTime.getTime(), -period);
}public void scheduleAtFixedRate(TimerTask task, long delay, long period) {if (delay < 0)throw new IllegalArgumentException("Negative delay.");if (period <= 0)throw new IllegalArgumentException("Non-positive period.");sched(task, System.currentTimeMillis()+delay, period);
}public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period) {if (period <= 0)throw new IllegalArgumentException("Non-positive period.");sched(task, firstTime.getTime(), period);
}

两个方法最终调用sched(TimerTask task, long time, long period) 方法,注意schedule固定延迟方法在period大于零的条件下传值为-period,这点是为了与固定scheduleAtFixedRate区分开。

下面研究进入sched(TimerTask task, long time, long period) 方法:

private void sched(TimerTask task, long time, long period) {/**省略部分代码*/synchronized(queue) {if (!thread.newTasksMayBeScheduled)throw new IllegalStateException("Timer already cancelled.");synchronized(task.lock) {//如果任务状态不是新建状态,抛出异常if (task.state != TimerTask.VIRGIN)throw new IllegalStateException("Task already scheduled or cancelled");//设置下次执行时间task.nextExecutionTime = time;task.period = period;//更新任务状态为调度状态task.state = TimerTask.SCHEDULED;}//加入队列中queue.add(task);//队列由空队列变为非空,唤醒队列为空时阻塞的线程if (queue.getMin() == task)queue.notify();}
}

在这个方法中只看到了加入队列的逻辑,任务的执行逻辑没有。

其实任务的执行逻辑在前面看任务执行线程TimerThread的代码时已经出现,而且重点在mainLoop方法中。回看mainLoop方法源码,我们发现方法第4步:判断任务状态,一次性任务直接从队列删除即可;重复性任务需要重新设定下次执行时间,然后放入队列。重点!重点!重点!重要的事情说三遍。

仔细看下这段逻辑:

queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period);

rescheduleMin方法代码如下,设置队列第一个元素(即任务)的下次执行时间为给定的newTime值,同时重排序队列。

    void rescheduleMin(long newTime) {        queue[1].nextExecutionTime = newTime;fixDown(1);}

所以mainLoop方法第4步的逻辑可以解释为:

  • 当period小于0时,即固定延迟的任务时,设置任务下次执行时间nextExecutionTime 为当前时间-period;别忘了,上面我们说过schedule方法传值为 -原period,即我们设定的延迟 原period=-period,所以这里其实就是设置下次执行时间为当前时间+原period
  • 当period大于0时,即固定频率的任务时,设置任务下次执行时间nextExecutionTime 为nextExecutionTime+period,所以只要开始时间确定了,nextExecutionTime也就固定了,不会有改变。

这就是两者的区别,那怎么证明呢,接着往下看。

修改MyTimeTask类run方法:

public void run() {log.info("sleep start,nextExcuteTime={}",DateFormatUtil.getFormatter().format(nextExecutionTime));try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}log.info("sleep end,nextExcuteTime={}",DateFormatUtil.getFormatter().format(nextExecutionTime));
}

使用schedule方法测试,代码不变,结果如下:

图片中c1、c2、c3是任务开始的当前时间,也即currentTime,n1、n2、n3是任务的下次执行时间,可以得出以下关系:

n1 = c1 + 2s、n2 = c2 + 2s、n3 = c3 + 2s。

再换scheduleAtFixedRate方法测试,结果如下:

从时间上看,任务下次执行时间n与任务执行当前时间毫无关系,但n2 = n1 +2s,n3 = n2 +2s,n4 = n3 +2s。

结论:

  1. schedule方法按固定延迟执行任务,指的是每次都设定任务的下次执行时间为任务执行当前时间+延迟时间,即nextExecutionTime = currentTime + period
  2. scheduleAtFixedRate方法安装固定频率执行任务,指的是每次都设定任务的下次执行时间为现有值+延迟时间,即nextExecutionTime = nextExecutionTime + period

这个问题搞清楚,但是当任务执行时间为5s时完全没有按照设定的频率执行,这又是什么问题呢?

其实仔细想一想,虽然任务设定了下次执行时间,但是任务的最终执行要靠Timer中的TimerThread线程,而TimerThread是单线程的,所以它的调度频率势必影响任务的执行频率;如果从队列中获取的任务执行时间过长,那么下一个任务的执行时间也要延长,这就是测试中执行时间和设定的下次执行时间不完全一致的情况。

Java Timer相关推荐

  1. java Timer定时器管理类

    1.java timer类,定时器类.启动执行定时任务方法是timer.schedule(new RemindTask(), seconds*1000);俩参数分别是TimerTask子类,具体执行定 ...

  2. java timer开销_减少Java垃圾收集开销的5条提示

    java timer开销 保持较低的GC开销的一些最有用的技巧是什么? 随着Java 9即将再次延迟发布,G1("垃圾优先")垃圾收集器将设置为HotSpot JVM的默认收集器. ...

  3. java+timer+退出,java timer 兑现在指定时间执行方法

    java timer 实现在指定时间执行方法 最近在弄一个定时发送的项目,需要定时执行方法,于是用到了Timer,但是我想要的功能网络上的资源比较少,所以自己研究了一下. 下面是我写的一些代码 imp ...

  4. Java Timer、TimerTask(定时任务)

    >java.util.Timer 一种工具,线程用其安排以后在后台线程中执行的任务.可安排任务执行一次,或者定期重复执行. TimerTask实现了Runnable接口,作为定时执行的任务载体. ...

  5. JAVA Timer 定时器

    JAVA  Timer 定时器测试 MyTask.java: package com.timer; import java.text.SimpleDateFormat; import java.uti ...

  6. Java Timer TimerTask示例

    Java java.util.Timer is a utility class that can be used to schedule a thread to be executed at cert ...

  7. java timer指定线程池_Java 定时器(Timer)及线程池里使用定时器实例代码

    java Timer定时器 简单实例代码:public class Test { public static void main(String[] args) { // Timer定时器 Timer ...

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

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

  9. Java Timer 定时器的使用

    一.延时执行首先,我们定义一个类,给它取个名字叫TimeTask,我们的定时任务,就在这个类的main函数里执行. 代码如下: package test; import java.util.Timer ...

  10. Java Timer定时器 使用

    2019独角兽企业重金招聘Python工程师标准>>> Java 自带的定时器,有两个重要的类:TimerTask和Timer. 如下: 简单的使用: package com;imp ...

最新文章

  1. linux设置密码过期时间
  2. Java设计模式 创建模式-单态模式(Singleton)
  3. python程序控制结构_python程序控制结构
  4. 雷达成像技术_毫米波立体成像雷达技术研究
  5. Vim中数字自增、自减
  6. 10 张图带你深入理解Docker容器和镜像
  7. 微信小程序request请求动态获取数据
  8. hadoop 2.5.0安装和配置
  9. mysql授权远程访问
  10. 深入进货单-期初单据
  11. Android零基础入门第66节:RecyclerView点击事件处理
  12. 《游戏设计艺术(第二版)》第十二章个人学习
  13. 杆刚度校核c语言程序,c语言 求解单元刚度矩阵
  14. jaxen-1.1-beta-6.jar下载,Dom4j的xpath的使用
  15. bin和cue怎么合并_bin和cue格式的文件怎么用?
  16. js 获取服务器时间——IE浏览器出现1970问题
  17. 用户画像业务数据调研及ETL(二)持续更新中...
  18. android 11.0 wifi开关控制
  19. 关于鸿蒙,你怎么看,三面美团Android岗
  20. GetSystemInfo系统信息

热门文章

  1. 下载网页或者微信公众号中视频的方法详细介绍
  2. 计算机与三菱plc485通讯,三菱plc同三菱变频器RS-485通讯功能的编程实例
  3. 双足机器人的稳定性判据_仿人双足机器人步态规划——零力矩点(ZMP)
  4. Pr 音频效果参考:混响
  5. iOS逆向之分析工具的安装和使用
  6. 西门子PLC丨PROFINET通讯仿真(虚拟通讯)
  7. MySQL 8.0中的新增功能
  8. 多源信息融合技术研究综述
  9. 芯片手册不需要全部看,抓住框架信息即可
  10. win10编译OpenCV4Android系列2-编译OpenCV4.5.2+opencv_contrib