关于定时任务:Java中定时任务的6种实现方式你知道几种

33次阅读

共计 10170 个字符,预计需要花费 26 分钟才能阅读完成。

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

所以,零碎的学习一下定时工作,是十分有必要的。这篇文章就带大家整体梳理学习一下 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

正文完
 0