3

Java Timer使用介绍 - UnicornLien

 1 year ago
source link: https://www.cnblogs.com/ladderx/p/16848561.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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. void schedule(TimerTask task, long delay):延迟delay毫秒后执行一次task
  2. void schedule(TimerTask task, Date time):在指定时间time执行一次task,如果time过期,将会立即执行

二、可重复执行任务

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

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

  1. void schedule(TimerTask task, long delay, long period):延迟delay毫秒后执行task,之后每隔period毫秒执行一次task
  2. void schedule(TimerTask task, Date firstTime, long period):在指定时间time执行一次task,之后每隔period毫秒执行一次task
  3. void scheduleAtFixedRate(TimerTask task, long delay, long period):延迟delay毫秒后执行task,之后每隔period毫秒执行一次task
  4. void scheduleAtFixedRate(TimerTask task, Date firstTime, long period):在指定时间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的处理方式不同,后面介绍。

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

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

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

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

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

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

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

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

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

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

固定速率就好比你今天加班到很晚,但是到了第二天还必须准点到公司上班,如果你一不小心加班到了第二天早上 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秒执行一个任务。

  1. 如果改为schedule(TimerTask task, Date firstTime, long period)和scheduleAtFixedRate(TimerTask task, Date firstTime, long period)来调度任务,firstTime指定为10点,而当前系统时间为11点,会出现什么情况呢?

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

  1. 如果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(boolean isDaemon)
  • Timer(String name, boolean isDaemon)

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK