简直在所有的我的项目中,定时工作的应用都是不可或缺的,如果使用不当甚至会造成资损。还记得多年前在做金融零碎时,出款业务是通过定时工作对外打款,过后因为银行接口解决能力无限,外加定时工作使用不当,导致收回大量反复出款申请。还好在前面环节将交易卡在了零碎外部,未产生资损。

所以,零碎的学习一下定时工作,是十分有必要的。这篇文章就带大家整体梳理学习一下Java畛域中常见的几种定时工作实现。

线程期待实现

先从最原始最简略的形式来解说。能够先创立一个thread,而后让它在while循环里始终运行着,通过sleep办法来达到定时工作的成果。

public class Task {    public static void main(String[] args) {        // run in a second        final long timeInterval = 1000;        Runnable runnable = new Runnable() {            @Override            public void run() {                while (true) {                    System.out.println("Hello !!");                    try {                        Thread.sleep(timeInterval);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            }        };        Thread thread = new Thread(runnable);        thread.start();    }}

这种形式简略间接,然而可能实现的性能无限,而且须要本人来实现。

JDK自带Timer实现

目前来看,JDK自带的Timer API算是最古老的定时工作实现形式了。Timer是一种定时器工具,用来在一个后盾线程打算执行指定工作。它能够安顿工作“执行一次”或者定期“执行屡次”。

在理论的开发当中,常常须要一些周期性的操作,比方每5分钟执行某一操作等。对于这样的操作最不便、高效的实现形式就是应用java.util.Timer工具类。

外围办法

Timer类的外围办法如下:

// 在指定延迟时间后执行指定的工作schedule(TimerTask task,long delay);// 在指定工夫执行指定的工作。(只执行一次)schedule(TimerTask task, Date time);// 提早指定工夫(delay)之后,开始以指定的距离(period)反复执行指定的工作schedule(TimerTask task,long delay,long period);// 在指定的工夫开始依照指定的距离(period)反复执行指定的工作schedule(TimerTask task, Date firstTime , long period);// 在指定的工夫开始进行反复的固定速率执行工作scheduleAtFixedRate(TimerTask task,Date firstTime,long period);// 在指定的提早后开始进行反复的固定速率执行工作scheduleAtFixedRate(TimerTask task,long delay,long period);// 终止此计时器,抛弃所有以后已安顿的工作。cancal();// 从此计时器的工作队列中移除所有已勾销的工作。purge();

应用示例

上面用几个示例演示一下外围办法的应用。首先定义一个通用的TimerTask类,用于定义用执行的工作。

public class DoSomethingTimerTask extends TimerTask {    private String taskName;    public DoSomethingTimerTask(String taskName) {        this.taskName = taskName;    }    @Override    public void run() {        System.out.println(new Date() + " : 工作「" + taskName + "」被执行。");    }}

指定提早执行一次

在指定延迟时间后执行一次,这类是比拟常见的场景,比方:当零碎初始化某个组件之后,提早几秒中,而后进行定时工作的执行。

public class DelayOneDemo {    public static void main(String[] args) {        Timer timer = new Timer();        timer.schedule(new DoSomethingTimerTask("DelayOneDemo"),1000L);    }}

执行上述代码,提早一秒之后执行定时工作,并打印后果。其中第二个参数单位为毫秒。

固定距离执行

在指定的延迟时间开始执行定时工作,定时工作依照固定的距离进行执行。比方:提早2秒执行,固定执行距离为1秒。

public class PeriodDemo {    public static void main(String[] args) {        Timer timer = new Timer();        timer.schedule(new DoSomethingTimerTask("PeriodDemo"),2000L,1000L);    }}

执行程序,会发现2秒之后开始每隔1秒执行一次。

固定速率执行

在指定的延迟时间开始执行定时工作,定时工作依照固定的速率进行执行。比方:提早2秒执行,固定速率为1秒。

public class FixedRateDemo {    public static void main(String[] args) {        Timer timer = new Timer();        timer.scheduleAtFixedRate(new DoSomethingTimerTask("FixedRateDemo"),2000L,1000L);    }}

执行程序,会发现2秒之后开始每隔1秒执行一次。

此时,你是否纳闷schedule与scheduleAtFixedRate成果一样,为什么提供两个办法,它们有什么区别?

schedule与scheduleAtFixedRate区别

在理解schedule与scheduleAtFixedRate办法的区别之前,先看看它们的相同点:

  • 工作执行未超时,下次执行工夫 = 上次执行开始工夫 + period;
  • 工作执行超时,下次执行工夫 = 上次执行完结工夫;

在工作执行未超时时,它们都是上次执行工夫加上间隔时间,来执行下一次工作。而执行超时时,都是立马执行。

它们的不同点在于侧重点不同,schedule办法偏重放弃间隔时间的稳固,而scheduleAtFixedRate办法更加侧重于放弃执行频率的稳固。

schedule偏重放弃间隔时间的稳固

schedule办法会因为前一个工作的提早而导致其前面的定时工作延时。计算公式为scheduledExecutionTime(第n+1次) = realExecutionTime(第n次) + periodTime。

也就是说如果第n次执行task时,因为某种原因这次执行工夫过长,执行完后的systemCurrentTime>= scheduledExecutionTime(第n+1次),则此时不做时隔期待,立刻执行第n+1次task。

而接下来的第n+2次task的scheduledExecutionTime(第n+2次)就随着变成了realExecutionTime(第n+1次)+periodTime。这个办法更重视放弃间隔时间的稳固。

scheduleAtFixedRate放弃执行频率的稳固

scheduleAtFixedRate在重复执行一个task的打算时,每一次执行这个task的打算执行工夫在最后就被定下来了,也就是scheduledExecutionTime(第n次)=firstExecuteTime +n*periodTime。

如果第n次执行task时,因为某种原因这次执行工夫过长,执行完后的systemCurrentTime>= scheduledExecutionTime(第n+1次),则此时不做period距离期待,立刻执行第n+1次task。

接下来的第n+2次的task的scheduledExecutionTime(第n+2次)仍然还是firstExecuteTime+(n+2)*periodTime这在第一次执行task就定下来了。说白了,这个办法更重视放弃执行频率的稳固。

如果用一句话来形容工作执行超时之后schedule和scheduleAtFixedRate的区别就是:schedule的策略是错过了就错过了,后续依照新的节奏来走;scheduleAtFixedRate的策略是如果错过了,就致力追上原来的节奏(制订好的节奏)。

Timer的缺点

Timer计时器能够定时(指定工夫执行工作)、提早(提早5秒执行工作)、周期性地执行工作(每隔个1秒执行工作)。然而,Timer存在一些缺点。首先Timer对调度的反对是基于相对工夫的,而不是绝对工夫,所以它对系统工夫的扭转十分敏感。

其次Timer线程是不会捕捉异样的,如果TimerTask抛出的了未查看异样则会导致Timer线程终止,同时Timer也不会从新复原线程的执行,它会谬误的认为整个Timer线程都会勾销。同时,曾经被安顿单尚未执行的TimerTask也不会再执行了,新的工作也不能被调度。故如果TimerTask抛出未查看的异样,Timer将会产生无奈意料的行为。

JDK自带ScheduledExecutorService

ScheduledExecutorService是JAVA 1.5后新增的定时工作接口,它是基于线程池设计的定时工作类,每个调度工作都会调配到线程池中的一个线程去执行。也就是说,工作是并发执行,互不影响。

须要留神:只有当执行调度工作时,ScheduledExecutorService才会真正启动一个线程,其余工夫ScheduledExecutorService都是出于轮询工作的状态。

ScheduledExecutorService次要有以下4个办法:

ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);<V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnitunit);ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnitunit);

其中scheduleAtFixedRate和scheduleWithFixedDelay在实现定时程序时比拟不便,使用的也比拟多。

ScheduledExecutorService中定义的这四个接口办法和Timer中对应的办法简直一样,只不过Timer的scheduled办法须要在内部传入一个TimerTask的形象工作。
而ScheduledExecutorService封装的更加粗疏了,传Runnable或Callable外部都会做一层封装,封装一个相似TimerTask的形象工作类(ScheduledFutureTask)。而后传入线程池,启动线程去执行该工作。

scheduleAtFixedRate办法

scheduleAtFixedRate办法,按指定频率周期执行某个工作。定义及参数阐明:

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

参数对应含意:command为被执行的线程;initialDelay为初始化后延时执行工夫;period为两次开始执行最小间隔时间;unit为计时单位。

应用实例:

public class ScheduleAtFixedRateDemo implements Runnable{    public static void main(String[] args) {        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);        executor.scheduleAtFixedRate(                new ScheduleAtFixedRateDemo(),                0,                1000,                TimeUnit.MILLISECONDS);    }    @Override    public void run() {        System.out.println(new Date() + " : 工作「ScheduleAtFixedRateDemo」被执行。");        try {            Thread.sleep(2000L);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

下面是scheduleAtFixedRate办法的根本应用形式,但当执行程序时会发现它并不是距离1秒执行的,而是距离2秒执行。

这是因为,scheduleAtFixedRate是以period为距离来执行工作的,如果工作执行工夫小于period,则上次工作执行实现后会距离period后再去执行下一次工作;但如果工作执行工夫大于period,则上次工作执行结束后会不距离的立刻开始下次工作。

scheduleWithFixedDelay办法

scheduleWithFixedDelay办法,按指定频率距离执行某个工作。定义及参数阐明:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,                long initialDelay,                long delay,                TimeUnit unit);

参数对应含意:command为被执行的线程;initialDelay为初始化后延时执行工夫;period为前一次执行完结到下一次执行开始的间隔时间(距离执行延迟时间);unit为计时单位。

应用实例:

public class ScheduleAtFixedRateDemo implements Runnable{    public static void main(String[] args) {        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);        executor.scheduleWithFixedDelay(                new ScheduleAtFixedRateDemo(),                0,                1000,                TimeUnit.MILLISECONDS);    }    @Override    public void run() {        System.out.println(new Date() + " : 工作「ScheduleAtFixedRateDemo」被执行。");        try {            Thread.sleep(2000L);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

下面是scheduleWithFixedDelay办法的根本应用形式,但当执行程序时会发现它并不是距离1秒执行的,而是距离3秒。

这是因为scheduleWithFixedDelay是不论工作执行多久,都会等上一次工作执行结束后再提早delay后去执行下次工作。

Quartz框架实现

除了JDK自带的API之外,咱们还能够应用开源的框架来实现,比方Quartz。

Quartz是Job scheduling(作业调度)畛域的一个开源我的项目,Quartz既能够独自应用也能够跟spring框架整合应用,在理论开发中个别会应用后者。应用Quartz能够开发一个或者多个定时工作,每个定时工作能够独自指定执行的工夫,例如每隔1小时执行一次、每个月第一天上午10点执行一次、每个月最初一天下午5点执行一次等。

Quartz通常有三局部组成:调度器(Scheduler)、工作(JobDetail)、触发器(Trigger,包含SimpleTrigger和CronTrigger)。上面以具体的实例进行阐明。

Quartz集成

要应用Quartz,首先须要在我的项目的pom文件中引入相应的依赖:

<dependency>    <groupId>org.quartz-scheduler</groupId>    <artifactId>quartz</artifactId>    <version>2.3.2</version></dependency><dependency>    <groupId>org.quartz-scheduler</groupId>    <artifactId>quartz-jobs</artifactId>    <version>2.3.2</version></dependency>

定义执行工作的Job,这里要实现Quartz提供的Job接口:

public class PrintJob implements Job {    @Override    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {        System.out.println(new Date() + " : 工作「PrintJob」被执行。");    }}

创立Scheduler和Trigger,并执行定时工作:

public class MyScheduler {    public static void main(String[] args) throws SchedulerException {        // 1、创立调度器Scheduler        SchedulerFactory schedulerFactory = new StdSchedulerFactory();        Scheduler scheduler = schedulerFactory.getScheduler();        // 2、创立JobDetail实例,并与PrintJob类绑定(Job执行内容)        JobDetail jobDetail = JobBuilder.newJob(PrintJob.class)                .withIdentity("job", "group").build();        // 3、构建Trigger实例,每隔1s执行一次        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger", "triggerGroup")                .startNow()//立刻失效                .withSchedule(SimpleScheduleBuilder.simpleSchedule()                        .withIntervalInSeconds(1)//每隔1s执行一次                        .repeatForever()).build();//始终执行        //4、Scheduler绑定Job和Trigger,并执行        scheduler.scheduleJob(jobDetail, trigger);        System.out.println("--------scheduler start ! ------------");        scheduler.start();    }}

执行程序,能够看到每1秒执行一次定时工作。

在上述代码中,其中Job为Quartz的接口,业务逻辑的实现通过实现该接口来实现。

JobDetail绑定指定的Job,每次Scheduler调度执行一个Job的时候,首先会拿到对应的Job,而后创立该Job实例,再去执行Job中的execute()的内容,工作执行完结后,关联的Job对象实例会被开释,且会被JVM GC革除。

Trigger是Quartz的触发器,用于告诉Scheduler何时去执行对应Job。SimpleTrigger能够实现在一个指定时间段内执行一次作业工作或一个时间段内屡次执行作业工作。

CronTrigger性能十分弱小,是基于日历的作业调度,而SimpleTrigger是精准指定距离,所以相比SimpleTrigger,CroTrigger更加罕用。CroTrigger是基于Cron表达式的。

常见的Cron表达式示例如下:

能够看出,基于Quartz的CronTrigger能够实现十分丰盛的定时工作场景。

Spring Task

从Spring 3开始,Spring自带了一套定时工作工具Spring-Task,能够把它看成是一个轻量级的Quartz,应用起来非常简略,除Spring相干的包外不须要额定的包,反对注解和配置文件两种模式。通常状况下在Spring体系内,针对简略的定时工作,可间接应用Spring提供的性能。

基于XML配置文件的模式就不再介绍了,间接看基于注解模式的实现。应用起来非常简单,间接上代码:

@Component("taskJob")public class TaskJob {    @Scheduled(cron = "0 0 3 * * ?")    public void job1() {        System.out.println("通过cron定义的定时工作");    }    @Scheduled(fixedDelay = 1000L)    public void job2() {        System.out.println("通过fixedDelay定义的定时工作");    }    @Scheduled(fixedRate = 1000L)    public void job3() {        System.out.println("通过fixedRate定义的定时工作");    }}

如果是在Spring Boot我的项目中,须要在启动类上增加@EnableScheduling来开启定时工作。

上述代码中,@Component用于实例化类,这个与定时工作无关。@Scheduled指定该办法是基于定时工作进行执行,具体执行的频次是由cron指定的表达式所决定。对于cron表达式下面CronTrigger所应用的表达式统一。与cron对照的,Spring还提供了fixedDelay和fixedRate两种模式的定时工作执行。

fixedDelay和fixedRate的区别

fixedDelay和fixedRate的区别于Timer中的区别很类似。

fixedRate有一个时刻表的概念,在工作启动时,T1、T2、T3就曾经排好了执行的时刻,比方1分、2分、3分,当T1的执行工夫大于1分钟时,就会造成T2晚点,当T1执行完时T2立刻执行。

fixedDelay比较简单,示意上个工作完结,到下个工作开始的工夫距离。无论工作执行破费多少工夫,两个工作间的距离始终是统一的。

Spring Task的毛病

Spring Task 自身不反对长久化,也没有推出官网的分布式集群模式,只能靠开发者在业务利用中本人手动扩大实现,无奈满足可视化,易配置的需要。

分布式任务调度

以上定时工作计划都是针对单机的,只能在单个JVM过程中应用。而当初基本上都是分布式场景,须要一套在分布式环境下高性能、高可用、可扩大的分布式任务调度框架。

Quartz分布式

首先,Quartz是能够用于分布式场景的,但须要基于数据库锁的模式。简略来说,quartz的散布式调度策略是以数据库为边界的一种异步策略。各个调度器都恪守一个基于数据库锁的操作规定从而保障了操作的唯一性,同时多个节点的异步运行保障了服务的牢靠。

因而,Quartz的分布式计划只解决了工作高可用(缩小单点故障)的问题,解决能力瓶颈在数据库,而且没有执行层面的工作分片,无奈最大化效率,只能依附shedulex调度层面做分片,然而调度层做并行分片难以结合实际的运行资源状况做最优的分片。

轻量级神器XXL-Job

XXL-JOB是一个轻量级分布式任务调度平台。特点是平台化,易部署,开发迅速、学习简略、轻量级、易扩大。由调度核心和执行器性能实现定时工作的执行。调度核心负责对立调度,执行器负责接管调度并执行。

针对于中小型我的项目,此框架使用的比拟多。

其余框架

除此之外,还有Elastic-Job、Saturn、SIA-TASK等。

Elastic-Job具备高可用的个性,是一个散布式调度解决方案。

Saturn是唯品会开源的一个分布式任务调度平台,在Elastic Job的根底上进行了革新。

SIA-TASK是宜信开源的分布式任务调度平台。

小结

通过本文梳理了6种定时工作的实现,就实际场景的使用来说,目前大多数零碎曾经脱离了单机模式。对于并发量并不是太高的零碎,xxl-job或者是一个不错的抉择。

源码地址:https://github.com/secbr/java...

博主简介:《SpringBoot技术底细》技术图书作者,热爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢送关注~

技术交换:请分割博主微信号:zhuan2quan