程序地带

分享一篇详解介绍Java中定时任务Timer、Spring Task、quartz


现有的定时任务技术
Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。使用较少。(不推荐使用,代码案例中已经给出说明)Spring3.0以后自主开发的定时任务工具spring task,使用简单,支持线程池,可以高效处理许多不同的定时任务,除spring相关的包外不需要额外的包,支持注解和配置文件两种形式。 不能处理过于复杂的任务专业的定时框架quartz,功能强大,可以让你的程序在指定时间执行,也可以按照某一个频度执行,支持数据库、监听器、插件、集群
代码实例
1.Timer
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @Author: njitzyd
* @Date: 2021/1/14 22:27
* @Description: Java自带的Timer类
* @Version 1.0.0
*/
public class MyTimer {
public static void main(String[] args) {
// 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。
//
// //org.apache.commons.lang3.concurrent.BasicThreadFactory
// ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
// new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
// executorService.scheduleAtFixedRate(new Runnable() {
// @Override
// public void run() {
// //do something
// }
// },initialDelay,period, TimeUnit.HOURS);
try {
// 创建定时器
Timer timer = new Timer();
// 添加调度任务
// 安排指定的任务在指定的时间开始进行重复的 固定延迟执行
timer.schedule(new MyTask(),new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2021-01-14 22:43:10"),10*1000);
// 安排指定的任务在指定的延迟后开始进行重复的 固定速率执行
//timer.scheduleAtFixedRate(new MyTask(),new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2021-01-14 22:43:10"),10*1000);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
/**
* 自定义的任务类
*/
class MyTask extends TimerTask {
// 定义调度任务
public void run() {
System.out.println("log2:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
2.Spring Task

配置有两种方式,一种是基于注解,一种是基于配置文件。在springboot中推荐使用注解和配置类的方式,这里我们主要使用注解和配置类,基于配置文件的也会给出demo。


基于注解

在springboot的启动类上通过注解@EnableScheduling开启。然后在类的方法上通过@Scheduled注解使用,代码案例如下:


@Component
public class ScheduleTest {
@Scheduled(fixedDelayString = "5000")
public void testFixedDelayString() {
System.out.println("Execute at " + System.currentTimeMillis());
}
}

具体的使用可以参考我的另一篇博客:@shcedule注解的使用


基于xml配置

首先是任务类:


/**
* 任务类
* @author 朱友德
*/
public class SpringTask {
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public void m1(){
System.out.println("m1:"+simpleDateFormat.format(new Date()));
}
public void m2(){
System.out.println("m2:"+simpleDateFormat.format(new Date()));
}
public void m3(){
System.out.println("m2:"+simpleDateFormat.format(new Date()));
}
}

然后是xml配置:


<!--spring-task.xml配置-->
<bean id="springTask" class="com.njit.springtask.SpringTask"></bean>
<!--注册调度任务-->
<task:scheduled-tasks>
<!--延迟8秒 执行任务-->
<!--<task:scheduled ref="springTask" method="m1" fixed-delay="8000" />-->
<!--固定速度5秒 执行任务-->
<!--<task:scheduled ref="springTask" method="m2" fixed-rate="5000"/>-->
<!--
使用cron表达式 指定触发时间
spring task 只支持6位的cron表达式 秒 分 时 日 月 星期
-->
<task:scheduled ref="springTask" method="m3" cron="50-59 * * ? * *"/>
</task:scheduled-tasks>
<!--执行器配置-->
<task:executor id="threadPoolTaskExecutor" pool-size="10" keep-alive="5"></task:executor>
<!--调度器配置-->
<task:scheduler id="threadPoolTaskScheduler" pool-size="10"></task:scheduler>
3.quartz

首先我们要了解一下quartz中的一些基本概念:


Scheduler:任务调度器,是实际执行任务调度的控制器。在spring中通过SchedulerFactoryBean封装起来。

Trigger:触发器,用于定义任务调度的时间规则,有SimpleTrigger,CronTrigger,DateIntervalTrigger等,其中CronTrigger用的比较多,本文主要介绍这种方式。CronTrigger在spring中封装在CronTriggerFactoryBean中。


SimpleTrigger:简单触发器,从某个时间开始,每隔多少时间触发,重复多少次。CronTrigger:使用cron表达式定义触发的时间规则,如"0 0 0,2,4 1/1 ? " 表示每天的0,2,4点触发。DailyTimeIntervalTrigger:每天中的一个时间段,每N个时间单元触发,时间单元可以是毫秒,秒,分,小时CalendarIntervalTrigger:每N个时间单元触发,时间单元可以是毫秒,秒,分,小时,日,月,年。Calendar:它是一些日历特定时间点的集合。一个trigger可以包含多个Calendar,以便排除或包含某些时间点。JobDetail:用来描述Job实现类及其它相关的静态信息,如Job名字、关联监听器等信息。在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用。Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。实现Job接口的任务,默认是无状态的,若要将Job设置成有状态的(即是否支持并发),在quartz中是给实现的Job添加@DisallowConcurrentExecution注解
Quartz 任务调度的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任务调度的元数据, scheduler 是实际执行调度的控制器。

在 Quartz 中,trigger 是用于定义调度时间的元素,即按照什么时间规则去执行任务。Quartz 中主要提供了四种类型的 trigger:SimpleTrigger,CronTirgger,DailyTimeIntervalTrigger,和 CalendarIntervalTrigger


在 Quartz 中,job 用于表示被调度的任务。主要有两种类型的 job:无状态的(stateless)和有状态的(stateful)。对于同一个 trigger 来说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。Job 主要有两种属性:volatility 和 durability,其中 volatility 表示任务是否被持久化到数据库存储,而 durability 表示在没有 trigger 关联的时候任务是否被保留。两者都是在值为 true 的时候任务被持久化或保留。一个 job 可以被多个 trigger 关联,但是一个 trigger 只能关联一个 job


引入starter依赖
<!-- quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
编写两个任务Task
/**
* @author
* 任务一
*/
public class TestTask1 extends QuartzJobBean{
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("TestQuartz01----" + sdf.format(new Date()));
}
}
/**
* 任务二
* @author
*/
public class TestTask2 extends QuartzJobBean{
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("TestQuartz02----" + sdf.format(new Date()));
}
}
编写配置类
/**
* quartz的配置类
*/
@Configuration
public class QuartzConfig {
@Bean
public JobDetail testQuartz1() {
return JobBuilder.newJob(TestTask1.class).withIdentity("testTask1").storeDurably().build();
}
@Bean
public Trigger testQuartzTrigger1() {
//5秒执行一次
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(5)
.repeatForever();
return TriggerBuilder.newTrigger().forJob(testQuartz1())
.withIdentity("testTask1")
.withSchedule(scheduleBuilder)
.build();
}
@Bean
public JobDetail testQuartz2() {
return JobBuilder.newJob(TestTask2.class).withIdentity("testTask2").storeDurably().build();
}
@Bean
public Trigger testQuartzTrigger2() {
//cron方式,每隔5秒执行一次
return TriggerBuilder.newTrigger().forJob(testQuartz2())
.withIdentity("testTask2")
.withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
.build();
}
}
启动项目观察

可以正常的看到任务正常启动,任务Task被执行:


image-20210116214659029


实现原理
1.Timer

简单来说就是执行时把Task放到队列中,然后有个线程(注意他是单线程的,如果执行多个Task,一个抛出异常就会导致整个都蹦)会去拉取最近的任务(队列中是根据下次执行时间进行排序)去执行,如果时间没到则wait()方法等待。


而ScheduledThreadPoolExecutor的执行步骤是,执行时向队列中添加一条任务,队列内部根据执行时间顺序进行了排序。然后线程池中的线程来获取要执行的任务,如果任务还没到执行时间就在这等,等到任务可以执行,然后获取到ScheduledFutureTask执行,执行后修改下次的执行时间,再添加到队列中去。


ScheduledThreadPoolExecutor的运行机制


Timer的使用以及执行原理


2.spring task

在springboot中,使用`@schedule注解默认是单线程的,多个任务执行起来时间会有问题:B任务会因为A任务执行起来需要20S而被延后20S执行。所以我们有两个方案去解决这个问题


在方法上使用@Async注解指定线程池

这里主要介绍第二种,只需要配置一个配置类即可:


@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}

下面介绍原理:


jdk的线程池和任务调用器分别由ExecutorService、ScheduledExecutorService定义,继承关系如下:


11


ThreadPoolExecutor:ExecutorService的实现类,其构造函数提供了灵活的参数配置,可构造多种类型的线程池


ScheduledThreadPoolExecutor:ScheduledExecutorService的实现类,用于任务调度


spring task对定时任务的两个抽象:


TaskExecutor:与jdk中Executor相同,引入的目的是为定时任务的执行提供线程池的支持,如果设置,默认只有一个线程。TaskScheduler:提供定时任务支持,需要传入一个Runnable的任务做为参数,并指定需要周期执行的时间或者触发器,这样Runnable任务就可以周期性执行了。

继承关系如下:


22


任务执行器与调度器的实现类分别为ThreadPoolTaskExecutor、ThreadPoolTaskScheduler


TaskScheduler需要传入一个Runnable的任务做为参数,并指定需要周期执行的时间或者触发器(Trigger)。


spring定义了Trigger接口的实现类CronTrigger,支持使用cron表达式指定定时策略,使用如下:


scheduler.schedule(task, new CronTrigger("30 * * * * ?"));

在springboot项目中,我们一般都是使用@schedule注解来使用spring task,这个注解内部的实现就是使用上面的内容。


spring在初始化bean后,通过postProcessAfterInitialization拦截到所有的用到@Scheduled注解的方法,并解析相应的的注解参数,放入“定时任务列表”等待后续处理;之后再“定时任务列表”中统一执行相应的定时任务(任务为顺序执行,先执行cron,之后再执行fixedRate)


源码解析


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_24219625/article/details/112760973

随机推荐

JS中String对象和Array对象的常用方法

JS中String对象和Array对象的常用方法两个对象的方法可以从增、删、改、查、截取和其他方法这几个角度去理解和记忆,有些方法的名字和功能是相同的。1String对象1、字符串是不可...

Harlynn_ 阅读(832)

2020-11-29

能不能帮忙写一下啊大佬们女神让帮忙,图片质量有点差求求了...

m0_53074416 阅读(702)

JAVA读取csv文件

packageollie1;importjava.nio.charset.Charset;importjava.util.ArrayList;importjava.util.List;importco...

Ssbusbusbsubs 阅读(990)

ZYNQ学习笔记(芯片概述)

ZYNQ的本质特征就是组合了一个双核ARMCortexA9处理器和一个FPGAALTERA的NIOSSOPC,可编程片上系统,使用FPGA内部逻辑资源实现的处理器ÿ...

Ambitio-Roc. 阅读(924)

怎样才能写好用户故事(一)

作为一个BA,写解决方案(需求规格说明书)是最基本功。系统的每一个功能背后,都有一个用户故事在支撑。用户故事,描述了对用户有价值的...

luokai888 阅读(918)

iservice封装有哪些方法_封装详解

1、封装的作用和含义我要看电视,只需要按一下开关和换台就可以了。有必要了解电视机内部的结构吗?有必要碰碰显像管吗?制造厂家为了方便我们使用电视,把复杂的内部细节全部封装起来...

一颗大球糖bobo 阅读(519)