Java Timer使用讲解

作者:UnicornLien 时间:2023-11-28 20:30:33 

Timer 详解

Timer 和 TimerTask 用于在后台线程中调度任务的 java.util 类。 TimerTask 负责任务的执行, Timer 负责任务的调度。

定时功能

Timer 提供了三种定时模式:

fixed delay
fixed rate

java.util包下提供了对定时任务的支持,涉及2个类:

  1. Timer:定时器类

  2. TimerTask:任务抽象类

使用该定时任务我们需要继承TimerTask抽象类,覆盖run方法编写任务执行代码,并利用Timer定时器对TimerTask进行调度。

编写一个任务:

TimerTask task = new TimerTask() {
   @Override
   public void run() {
       System.out.println(DateUtil.formatNow() + " " + Thread.currentThread().getName() + " task run ");
   }
};

接着使用Timer对TimerTask进行调度,Timer提供了多种方法,可分为一次性任务和可重复执行任务。

一、一次性任务

一次性任务是指Timer执行一次之后,该任务后续不再执行。

一次性任务包括2个方法,如下:

  1. voidschedule(TimerTasktask,longdelay):延迟delay毫秒后执行一次task

  2. voidschedule(TimerTasktask,Datetime):在指定时间time执行一次task,如果time过期,将会立即执行

二、可重复执行任务

可重复执行任务是指,任务允许按照设定的规则重复执行。

可重复执行任务共有4个方法,分为固定延时 schedule和固定速率 scheduleAtFixedRate:

  1. voidschedule(TimerTasktask,longdelay,longperiod):延迟delay毫秒后执行task,之后每隔period毫秒执行一次task

  2. voidschedule(TimerTasktask,DatefirstTime,longperiod):在指定时间time执行一次task,之后每隔period毫秒执行一次task

  3. voidscheduleAtFixedRate(TimerTasktask,longdelay,longperiod):延迟delay毫秒后执行task,之后每隔period毫秒执行一次task

  4. voidscheduleAtFixedRate(TimerTasktask,DatefirstTime,longperiod):在指定时间time执行一次task,之后每隔period毫秒执行一次task

示例1:schedule方法,延迟delay毫秒后执行task,之后每隔period毫秒执行一次task

System.out.println("启动于:" + DateUtil.formatNow());
Timer timer = new Timer("timer");
timer.schedule(task, 1000, 2000);

输出:

启动于:2022-10-31 10:05:15
2022-10-31 10:05:16 timer task run 
2022-10-31 10:05:18 timer task run 
2022-10-31 10:05:20 timer task run

示例2:schedule在指定时间time执行一次task,之后每隔period毫秒执行一次task

System.out.println("启动于:" + DateUtil.formatNow());
Timer timer = new Timer("timer");
timer.schedule(task, DateUtil.parse("2022-10-31 10:07:00", DateUtil.YYYY_MM_DD_HH24_MM_SS), 2000);

输出:

启动于:2022-10-31 10:06:39
2022-10-31 10:07:00 timer task run 
2022-10-31 10:07:02 timer task run 
2022-10-31 10:07:04 timer task run 

固定延时 schedule 和 固定速率 scheduleAtFixedRate 在正常情况下看起来功能基本是一致的,区别在于当任务耗时超出执行时间间隔period,后续任务被延误时,schedule和scheduleAtFixedRate的处理方式不同,后面介绍。

三、固定延时和固定速率区别(重点)

1. 介绍

由于Timer内部仅维护一个线程来执行所有任务,所以当前一个任务耗时过长,可能会导致后一个任务的执行被延误。

出现任务延误的情况下,固定延时 schedule和 固定速率 scheduleAtFixedRate 的区别就在于,schedule会顺延,而scheduleAtFixedRate会把延误任务立马补上。

在网上看到几个非常恰当的例子,贴上来加深理解。

例1:

暑假到了老师给schedule和scheduleAtFixedRate两个同学布置作业。

老师要求学生暑假每天写2页,30天后完成作业。

这两个学生每天按时完成作业,直到第10天,出了意外,两个学生出去旅游花了5天时间,这5天时间里两个人都没有做作业。任务被拖延了。

这时候两个学生采取的策略就不同了:

schedule重新安排了任务时间,旅游回来的第一天做第11天的任务,第二天做第12天的任务,最后完成任务花了35天。

scheduleAtFixedRate是个守时的学生,她总想按时完成老师的任务,于是在旅游回来的第一天把之前5天欠下的任务以及第16天当天的任务全部完成了,之后还是按照老师的原安排完成作业,最后完成任务花了30天。

例2:

固定速率就好比你今天加班到很晚,但是到了第二天还必须准点到公司上班,如果你一不小心加班到了第二天早上 9 点,你就连休息的时间都没有了。

而固定时延的意思是你必须睡够 8 个小时再过来上班,如果你加班到凌晨 6 点,那就可以下午过来上班了。

固定速率强调准点,固定时延强调间隔。

如果任务必须每天准点调度,那就应该使用固定速率调度,并且要确保每个任务执行时间不要太长,避免超过period间隔。

如果任务需要每隔几分钟跑一次,那就使用固定时延调度,它不是很在乎单个任务要跑多长时间。

我们来模拟一下这个情况。

首先,我们对TimerTask进行修改,让它某一次任务产生大量耗时:

TimerTask task = new TimerTask() {
   private int i = 1;
   @Override
   public void run() {
       System.out.print(i + " " + DateUtil.formatNow() + " 开始执行, ");
       if(i == 3) {
           ThreadUtil.sleep(11 * 1000);
       }
       System.out.println(DateUtil.formatNow() + " 结束");
       i++;
   }
};

该任务在执行第3次时,将会休眠11秒,这将会导致延误后续的任务。

2. 固定速率

示例:

Timer timer = new Timer("timer");
timer.scheduleAtFixedRate(task, 5000, 2000);

设定任务延迟5秒后执行第1次任务,之后每2秒执行一次。

输出:

启动于:2022-10-31 15:51:24
1 2022-10-31 15:51:29 开始执行, 2022-10-31 15:51:29 结束
2 2022-10-31 15:51:31 开始执行, 2022-10-31 15:51:31 结束
3 2022-10-31 15:51:33 开始执行, 2022-10-31 15:51:44 结束 *
4 2022-10-31 15:51:44 开始执行, 2022-10-31 15:51:44 结束 *
5 2022-10-31 15:51:44 开始执行, 2022-10-31 15:51:44 结束 *
6 2022-10-31 15:51:44 开始执行, 2022-10-31 15:51:44 结束 *
7 2022-10-31 15:51:44 开始执行, 2022-10-31 15:51:44 结束 *
8 2022-10-31 15:51:44 开始执行, 2022-10-31 15:51:44 结束 *
9 2022-10-31 15:51:45 开始执行, 2022-10-31 15:51:45 结束
10 2022-10-31 15:51:47 开始执行, 2022-10-31 15:51:47 结束
11 2022-10-31 15:51:49 开始执行, 2022-10-31 15:51:49 结束

如果不存在第3次耗时11秒的情况下,正常任务执行时间应该为:

启动于:2022-10-31 15:51:24
1 2022-10-31 15:51:29 开始执行, 2022-10-31 15:51:29 结束
2 2022-10-31 15:51:31 开始执行, 2022-10-31 15:51:31 结束
3 2022-10-31 15:51:33 开始执行, 2022-10-31 15:51:33 结束 *
4 2022-10-31 15:51:35 开始执行, 2022-10-31 15:51:35 结束 *
5 2022-10-31 15:51:37 开始执行, 2022-10-31 15:51:37 结束 *
6 2022-10-31 15:51:39 开始执行, 2022-10-31 15:51:39 结束 *
7 2022-10-31 15:51:41 开始执行, 2022-10-31 15:51:41 结束 *
8 2022-10-31 15:51:43 开始执行, 2022-10-31 15:51:43 结束 *
9 2022-10-31 15:51:45 开始执行, 2022-10-31 15:51:45 结束
10 2022-10-31 15:51:47 开始执行, 2022-10-31 15:51:47 结束
11 2022-10-31 15:51:49 开始执行, 2022-10-31 15:51:49 结束

但是在第3次执行任务时因为执行耗时11秒,第4次本该在15:51:35开始执行并完成任务,却到了15:51:44才执行完成,这11秒延误了后续5个任务的正常执行,因此在15:51:44时,scheduleAtFixedRate赶作业把延误的5个任务一起执行了。

最后赶上了原本的进度,第9个任务准时在15:51:45执行。

3. 固定延时

示例:

Timer timer = new Timer("timer");
timer.schedule(task, 5000, 2000);

输出:

启动于:2022-10-31 15:56:59
1 2022-10-31 15:57:04 开始执行, 2022-10-31 15:57:04 结束
2 2022-10-31 15:57:06 开始执行, 2022-10-31 15:57:06 结束
3 2022-10-31 15:57:08 开始执行, 2022-10-31 15:57:19 结束 *
4 2022-10-31 15:57:19 开始执行, 2022-10-31 15:57:19 结束
5 2022-10-31 15:57:21 开始执行, 2022-10-31 15:57:21 结束
6 2022-10-31 15:57:24 开始执行, 2022-10-31 15:57:24 结束
7 2022-10-31 15:57:26 开始执行, 2022-10-31 15:57:26 结束
8 2022-10-31 15:57:28 开始执行, 2022-10-31 15:57:28 结束
9 2022-10-31 15:57:30 开始执行, 2022-10-31 15:57:30 结束
10 2022-10-31 15:57:32 开始执行, 2022-10-31 15:57:32 结束

如果不存在第3次耗时11秒的情况下,正常任务执行时间应该为:

启动于:2022-10-31 15:56:59
1 2022-10-31 15:57:04 开始执行, 2022-10-31 15:57:04 结束
2 2022-10-31 15:57:06 开始执行, 2022-10-31 15:57:06 结束
3 2022-10-31 15:57:08 开始执行, 2022-10-31 15:57:08 结束 *
4 2022-10-31 15:57:10 开始执行, 2022-10-31 15:57:10 结束
5 2022-10-31 15:57:12 开始执行, 2022-10-31 15:57:12 结束
6 2022-10-31 15:57:14 开始执行, 2022-10-31 15:57:14 结束
7 2022-10-31 15:57:16 开始执行, 2022-10-31 15:57:16 结束
8 2022-10-31 15:57:18 开始执行, 2022-10-31 15:57:18 结束
9 2022-10-31 15:57:20 开始执行, 2022-10-31 15:57:20 结束
10 2022-10-31 15:57:22 开始执行, 2022-10-31 15:57:22 结束

使用schedule调度,第4次任务本该在15:57:10开始执行,但由于耗时11秒直到15:57:19才开始。

而第3次任务实际是在19秒完成,完成后又在19秒立即执行第4次,中间少了2秒间隔,第4次完成后接着开始2秒一次,变为了从21秒开始执行第5次。

和我原本的推测不一样的是,本以为19秒完成后,第4次会隔2秒在21秒执行,没想到19秒会立即执行。

猜测与delay参数有关,但调整了delay后仍然一样,完成的那一秒还是会马上再执行第4次任务。

通过以上测试对比,我们可以感受到Timer中固定速率和固定延时的区别,但为了避免出错,使用Timer时应让TimerTask耗时尽可能短。

4. 其他要点

1.以上是仅第3次任务加上了耗时11秒,如果是所有任务都耗时11秒呢?

如果每次任务执行都耗时11秒,那么无论是固定速率还是固定延时,都将是11秒执行一个任务。

2.如果改为schedule(TimerTasktask,DatefirstTime,longperiod)和scheduleAtFixedRate(TimerTasktask,DatefirstTime,longperiod)来调度任务,firstTime指定为10点,而当前系统时间为11点,会出现什么情况呢?

虽然firstTime已经过期,但是Timer将会立即开始执行任务,之后按照period间隔重复执行任务。

3.如果TimerTask执行过程中抛出了异常会发生什么事情?

Timer内部仅维护一个线程,当任一TimerTask抛出异常,将导致此线程终止运行,该Timer负责的所有任务都无法执行。

四、调度多个TimerTask

在上一节中,介绍的是一个可重复执行的TimeTask,如果执行耗时大于设定的间隔period,将会影响该TimerTask下一次执行的时间点。

而这一节则是为了单独说明,一个Timer同时调度多个TimeTask也会互相影响。

示例:

TimerTask task1 = new TimerTask() {
   private int i = 1;
   @Override
   public void run() {
       System.out.print(i + " task1:" + DateUtil.formatNow() + " 开始执行, ");
       ThreadUtil.sleep(11 * 1000);
       System.out.println(DateUtil.formatNow() + " 结束");
       i++;
   }
};
TimerTask task2 = new TimerTask() {
   private int i = 1;
   @Override
   public void run() {
       System.out.print(i + "  task2:" + DateUtil.formatNow() + " 开始执行, ");
       ThreadUtil.sleep(11 * 1000);
       System.out.println(DateUtil.formatNow() + " 结束");
       i++;
   }
};

Timer timer = new Timer("timer");
timer.scheduleAtFixedRate(task1, 5000, 2000);
timer.scheduleAtFixedRate(task2, 5000, 2000);

输出:

1 task1:2022-10-31 16:58:27 开始执行, 2022-10-31 16:58:38 结束
1 task2:2022-10-31 16:58:38 开始执行, 2022-10-31 16:58:49 结束
2 task2:2022-10-31 16:58:49 开始执行, 2022-10-31 16:59:00 结束
2 task1:2022-10-31 16:59:00 开始执行, 2022-10-31 16:59:11 结束
3 task1:2022-10-31 16:59:11 开始执行, 2022-10-31 16:59:22 结束
3 task2:2022-10-31 16:59:22 开始执行, 2022-10-31 16:59:33 结束
4 task2:2022-10-31 16:59:33 开始执行, 2022-10-31 16:59:44 结束
4 task1:2022-10-31 16:59:44 开始执行, 2022-10-31 16:59:55 结束

可以发现,task1和task2其实都没有按照既定时间去执行任务了。

根本原因是在于,Timer内部仅维护一个线程执行所有TimerTask,为了避免错误,一个Timer对象最好仅调度一个TimerTask对象,除非可以确保多个TimerTask之间一定不会相互影响。

因此编写TimerTask时应当自行捕获异常。

五、取消任务

Timer在创建时实际上是默认在内部维护了一个非守护线程,即使任务全部执行完成,线程也并不会销毁。

Timer提供cancel()方法,可以手动调用取消定时器所有的任务,并销毁定时器。

如果想要Timer内部创建的是守护线程,可以使用以下构造方法创建定时器,设置isDaemon为true:

  • Timer(booleanisDaemon)

Timer(Stringname,booleanisDaemon)

如果没有自己定义name参数,默认Timer内部自动命名为“Timer-递增序号”,作为内部线程的线程名称,在构造方法内启动此线程。

如果是要取消单个任务,可以使用TimerTask的cancel()方法。

当TimerTask调用cancel之后,任务是取消了,但Timer自身并不能马上知道TimerTask被取消,而是在准备执行前才知道,因此Timer内部还维护着这个任务的引用。若希望Timer立即清除引用,可调用Timer.purge()立即执行清除。

来源:https://www.cnblogs.com/ladderx/p/16848561.html

标签:Java,Timer,使用
0
投稿

猜你喜欢

  • Android利用SAX对XML进行增删改查操作详解

    2023-02-05 00:03:32
  • Mybatis映射文件实例详解

    2023-06-12 13:08:42
  • 教你怎么用Java开发扫雷游戏

    2023-07-22 09:49:26
  • 使用IDEA开发配置Java Web的初始化过程

    2022-09-25 16:33:38
  • 如何使用C#修改本地Windows系统时间

    2022-05-06 08:23:00
  • Android上传文件到服务端并显示进度条

    2023-06-23 07:48:33
  • java贪吃蛇游戏编写代码

    2023-06-16 02:41:10
  • Mapreduce分布式并行编程

    2023-04-30 02:43:16
  • springboot为异步任务规划自定义线程池的实现

    2022-12-05 01:13:02
  • 基于C#实现简单的随机抽奖小程序

    2023-07-14 10:15:50
  • 浅谈Java三目运算

    2023-11-29 07:27:59
  • java数据结构-堆实现优先队列

    2023-11-25 08:30:20
  • Android studio实现简易计算器App功能

    2023-03-07 07:57:57
  • SpringBoot事件发布和监听详解

    2022-04-18 02:39:59
  • Tomcat 实现WebSocket详细介绍

    2023-08-12 21:22:11
  • C#最简单的关闭子窗体更新父窗体的实现方法

    2021-07-24 18:58:27
  • Spring实战之使用@Resource配置依赖操作示例

    2023-08-06 10:11:13
  • 详解spring中的Aware接口功能

    2023-07-02 00:36:01
  • java中String的一些方法深入解析

    2023-11-25 21:48:56
  • Android消息机制Handler用法总结

    2022-08-10 23:58:56
  • asp之家 软件编程 m.aspxhome.com