java定时工具的辟谣

作者: 江江的大猪 | 来源:发表于2017-11-20 20:31 被阅读313次

网络上关于java定时器的文章真的是错误百出,给我的学习造成了很大的困扰,Timer根本就没有线程安全问题,Timer的所有调度方法都和上次任务的结束时间没有关系,TImer和ScheduledThreadPoolExecutor当任务执行时间大于间隔时间的时候,该task不存在前一次执行还没结束,就再启动执行的情况。只有ScheduledThreadPoolExecutor对该task执行了多次schedulexxx方法、或者多个Timer schedule同一个task,会造成该task的并发。最后通过看源码做测试的方法整理了一些要点。

定时器Timer

Timer的作用是设置定时任务,但封装任务的类是TimerTask,需要程序员继承TimerTask这个抽象类,实现run()方法,放入自己的业务代码。


Timer的常用方法
  • Timer timer = new Timer()

构造TImer定时器,可以传入boolean参数,true将定时任务设置为守护线程,无参则不设置

  • 构建TimerTask实例

一般都是使用匿名类

TimerTask task = new TimerTask() {   
    public void run() {   
        ... //每次需要执行的业务代码
        cancel();//可以让该task退出调度
    }   
};
  • cancel取消全部定时任务

和TimerTask的cancel不同,将全部定时任务都取消

  • schedule调度定时任务

timer.schedule(task, time)

time为Date类型:在指定时间执行一次。

timer.schedule(task, delay)

delay 为long类型,不能小于0,从现在起过delay毫秒执行一次

timer.schedule(task, firstTime, period)

firstTime为Date类型,period为long,从firstTime时刻开始,每隔period毫秒执行一次

timer.schedule(task, delay, period)

delay为long,不能小于0,period为long,从现在起过delay毫秒以后,每隔period毫秒执行一次

tips

当任务的首次执行时间小于当前时间时,会立即执行,不会做补偿执行,起始时间按照当前时间为基准

验证代码:

Timer timer = new Timer();
//耗时2s左右的任务
TimerTask task1 = new TimerTask() {
    @Override
    public void run() {
        //获得该任务该次执行的期望开始时间
        System.out.println("expect start time: " + new Date(scheduledExecutionTime()));
        System.out.println("task1 start: " + new Date());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {}
        System.out.println("task1 end: " + new Date());
    }
};
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, -11);
Date date = calendar.getTime();
System.out.println("now: " + new Date());
System.out.println("timer start: " + date);
//设置调度的启动时间为11s前
timer.schedule(task1, date, 5000);

结果:

//当前时间16:05:51,设置的启动时间为16:05:40
now: Mon Nov 20 16:05:51 CST 2017
timer start: Mon Nov 20 16:05:40 CST 2017
//立即执行,并将期望的执行时间设为当前时间
expect start time: Mon Nov 20 16:05:51 CST 2017
task1 start: Mon Nov 20 16:05:51 CST 2017
task1 end: Mon Nov 20 16:05:53 CST 2017
//以实际的启动时间16:05:51为基准,间隔5秒周期执行,而不按照设置的16:05:40为基准
//下一次任务的开始时间以上次任务的开始时间为准,间隔5秒再次执行
expect start time: Mon Nov 20 16:05:56 CST 2017
task1 start: Mon Nov 20 16:05:56 CST 2017
task1 end: Mon Nov 20 16:05:58 CST 2017

可以看出当设置的启动时间(11秒前)小于当前时间时,任务立即执行,并且把当前时间当做基准,间隔5秒再次执行,没有将设置的启动时间为基准

  • scheduleAtFixedRate调度定时任务

timer.scheduleAtFixedRate(task, delay, period)

delay为long,period为long,从现在起过delay毫秒以后,每隔period毫秒执行一次

timer.scheduleAtFixedRate(task, firstTime, period)

firstTime为Date类型,period为long,从firstTime时刻开始,每隔period毫秒执行一次

tips

当任务的首次执行时间小于当前时间时,会立即执行,并将之前缺少执行的次数都执行回来,起始时间按照设定的初始时间为基准,atFixedRate的含义是以一种正确的速率执行,也就是一段时间内该执行几次就执行几次,因此会把之前缺少执行的次数补偿回来。

验证代码:

Timer timer = new Timer();
//耗时2s左右的任务
TimerTask task1 = new TimerTask() {
    @Override
    public void run() {
        //获得该任务该次执行的期望开始时间
        System.out.println("expect run time: " + new Date(scheduledExecutionTime()));
        System.out.println("task1 start: " + new Date());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println("task1 end: " + new Date());
    }
};
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, -11);
Date date = calendar.getTime();
System.out.println("now: " + new Date());
System.out.println("timer start: " + date);
//设置调度的启动时间为11s前
timer.scheduleAtFixedRate(task1, date, 5000);

结果:

//当前时间15:38:06,设置的启动时间为15:37:55
now: Mon Nov 20 15:38:06 CST 2017
timer start: Mon Nov 20 15:37:55 CST 2017
//期望开始时间小于实际开始时间,追赶进度
expect run time: Mon Nov 20 15:37:55 CST 2017
task1 start: Mon Nov 20 15:38:06 CST 2017
task1 end: Mon Nov 20 15:38:08 CST 2017
//期望开始时间小于实际开始时间,追赶进度
expect run time: Mon Nov 20 15:38:00 CST 2017
task1 start: Mon Nov 20 15:38:08 CST 2017
task1 end: Mon Nov 20 15:38:10 CST 2017
//期望开始时间小于实际开始时间,追赶进度
expect run time: Mon Nov 20 15:38:05 CST 2017
task1 start: Mon Nov 20 15:38:10 CST 2017
task1 end: Mon Nov 20 15:38:12 CST 2017
//期望开始时间小于实际开始时间,追赶进度
expect run time: Mon Nov 20 15:38:10 CST 2017
task1 start: Mon Nov 20 15:38:12 CST 2017
task1 end: Mon Nov 20 15:38:14 CST 2017
//进度追上了,以设置的启动时间15:37:55为基准
//没有按照上次任务的开始时间15:38:12间隔5秒执行
expect run time: Mon Nov 20 15:38:15 CST 2017
task1 start: Mon Nov 20 15:38:15 CST 2017
task1 end: Mon Nov 20 15:38:17 CST 2017
//从此开始正常执行
expect run time: Mon Nov 20 15:38:20 CST 2017
task1 start: Mon Nov 20 15:38:20 CST 2017
task1 end: Mon Nov 20 15:38:22 CST 2017
schedule和scheduleAtFixedRate总结
  1. 不同点:schedule和scheduleAtFixedRate只当timer启动时间小于当前时间时有区别, schedule不会把没执行的次数补回来,而是以当前时间为准,继续调度执行。scheduleAtFixedRate会按照设置的启动时间为准,将没执行的次数补回来
  2. 相同点:schedule和scheduleAtFixedRate都以上次任务的开始时间+period周期执行(scheduleAtFixedRate追赶进度时不是这样),和任务结束时间完全无关,不要被网上文章误导
  3. 相同点:当任务执行时间大于设置的period的时候,相当于period为0,只要任务执行完就立即再次执行。如果执行时间只是偶尔大于period的话,scheduleAtFixedRate可以补偿缺少执行的次数
  4. Timer内部只有一个线程执行TimerTask,因此TimerTask不存在线程安全问题,因为只有一个线程,所以Timer调度多个任务的时候是串行的,每个任务的开始时间受其他任务执行时间的影响。
    建议:Timer仅调度一个TimeTask时可以使用。其他情况ScheduledThreadPoolExecutor更好

ScheduledThreadPoolExecutor

由于Timer的种种缺陷,jdk5之后ScheduledThreadPoolExecutor是一种更好的定时器工具。ScheduledThreadPoolExecutor为了复用方法继承了ThreadPoolExecutor,但是ScheduledThreadPoolExecutor几个构造方法中只能设置corePoolSize。它作为一个使用corePoolSize线程和一个无界队列的固定大小的池,调整maximumPoolSize没有效果。一个线程负责一个schedulexxx方法的执行,corePoolSize小于执行的schedulexxx方法个数时,放入队列。


ScheduledThreadPoolExecutor的常用方法

ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

延迟delay个unit单位执行一次command,并返回一个future,该future的get只会是null

<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)

延迟delay个unit单位执行一次command,并返回一个future,future可以拿到callable的结果

ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit)

下次任务执行时间以上次任务结束时间为基准,initialDelay小于0设为0。任务执行时间大于period,相当于period为0,只要任务执行完就立即再次执行,不会新开一个线程并发执行

ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

下次任务执行时间以上次任务开始时间为基准, initialDelay小于0设为0。任务执行时间大于period,相当于period为0,只要任务执行完就立即再次执行,不会新开一个线程并发执行

future模式可以看这里

验证代码:

TimerTask task1 = new TimerTask() {
    @Override
    public void run() {
        //获得该任务该次执行的期望开始时间
        System.out.println("expect start time: " + new Date(scheduledExecutionTime()));
        System.out.println("task1 start: " + new Date());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println("task1 end: " + new Date());
    }
};
TimerTask task2 = new TimerTask() {
    @Override
    public void run() {
        //获得该任务该次执行的期望开始时间
        System.out.println("expect start time: " + new Date(scheduledExecutionTime()));
        System.out.println("task2 start: " + new Date());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println("task2 end: " + new Date());
    }
};
System.out.println("now: " + new Date());
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
executor.scheduleWithFixedDelay(task1, 0, 1000, TimeUnit.MILLISECONDS);
executor.scheduleAtFixedRate(task2, 0, 1000, TimeUnit.MILLISECONDS);

结果:

//可以看到ScheduledThreadPoolExecutor不会设置TimerTask的期望开始时间
//为了便于阅读,下面的这条日志删掉了
expect start time: Thu Jan 01 08:00:00 CST 1970
expect start time: Thu Jan 01 08:00:00 CST 1970
//两个任务分别占用一个线程,因此同时启动
task1 start: Mon Nov 20 19:58:24 CST 2017
task2 start: Mon Nov 20 19:58:24 CST 2017
//每个任务都耗时2s,虽然设定的period是1ms,但是任务执行期间并没有再次启动
task2 end: Mon Nov 20 19:58:26 CST 2017
task1 end: Mon Nov 20 19:58:26 CST 2017
//task2按照上次任务的开始时间+1s执行,因为实行时间(2s)大于1s,所以每次都会立即执行
task2 start: Mon Nov 20 19:58:26 CST 2017
//task1按照上次任务的结束时间(19:58:26)+1s执行,所以是19:58:27开始执行
task1 start: Mon Nov 20 19:58:27 CST 2017
task2 end: Mon Nov 20 19:58:28 CST 2017
task2 start: Mon Nov 20 19:58:28 CST 2017
task1 end: Mon Nov 20 19:58:29 CST 2017
task2 end: Mon Nov 20 19:58:30 CST 2017
  1. 可以看到ScheduledThreadPoolExecutor的两个周期执行方法的区别在于下次任务执行的之间参考点不一样,scheduleAtFixedRate以上次任务的开始时间算(所以起名为at fixed rate),scheduleWithFixedDelay以上次任务的结束时间算。
  2. 如果一个task对象只在一个schedulexxx方法中使用,是不存在并发问题的。
  3. ScheduledThreadPoolExecutor的线程数最好大于周期执行的任务数,否则使用队列的话,任务的开始执行时间还是会受到其他任务执行时间的影响,会有延迟。
ScheduledThreadPoolExecutor优点
  1. timer中一个任务抛出了异常,所有任务都停了,ScheduledThreadPoolExecutor中一个任务抛出了异常对其他任务没有影响,只是该任务不会再被周期的执行了。
  2. spring的定时任务基于ScheduledThreadPoolExecutor,使用方法看这里
肥肥小浣熊

相关文章

  • java定时工具的辟谣

    网络上关于java定时器的文章真的是错误百出,给我的学习造成了很大的困扰,Timer根本就没有线程安全问题,Tim...

  • Java定时任务调度工具详解

    本篇内容:什么是定时任务调度?Java定时任务调度工具详解之 Timer篇Java定时任务调度工具详解之 Quar...

  • Java定时任务工具详解之Timer篇

    Java定时任务调度工具详解 什么是定时任务调度? ◆ 基于给定的时间点,给定的时间间隔或者给定的执行次数自动执行...

  • java定时器

    java定时器 什么是Java定时器?Java 定时器就是在给定的间隔时间执行自己的任务; Java实现定时器有以...

  • Timer篇与 Quartz篇

    什么是定时任务调度 基于给定的时间点,给定的时间间隔或者给定的执行次数自动完成执行任务 在Java中的定时调度工具...

  • java使用Runtime.getRuntime().exec(

    最近需要用java程序做一个linux命令定时执行工具,看来看去java能运行外部linux命令的就只有Runti...

  • Java Web定时任务这一篇就够了

    一、Java定时任务 1、Timer java.util包下面一个工具类,从1.3开始便支持了; 说明下后两个参数...

  • 多线程之定时器任务

    在java中Timer是java.util包中的一个工具类,提供了定时器的功能。我们可以创建一个Timer对象,然...

  • Java的定时任务调度工具Timer

    一、 什么是java定时任务调度? 基于给定的时间点、给定的时间间隔、给定的执行次数自动执行的任务。 二、Java...

  • y3tu-tool工具

    y3tu-tool Java工具类,把平时工作学习中用的技术进行汇总,已供后续项目使用 使用 Todo 分布式定时...

网友评论

    本文标题:java定时工具的辟谣

    本文链接:https://www.haomeiwen.com/subject/hfjbvxtx.html