简介
文章中代码案例已经同步到码云:代码中的schedule-demo
中。
定时任务是指调度程序在指定的时间或周期触发执行的任务
使用场景:发送邮件、统计、状态修改、消息推送、活动开启、增量索引
现有的定时任务技术
- 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配置:
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依赖
org.springframework.boot
spring-boot-starter-quartz
- 编写两个任务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被执行:
实现原理
1.Timer
简单来说就是执行时把Task放到队列中,然后有个线程(注意他是单线程的,如果执行多个Task,一个抛出异常就会导致整个都蹦)会去拉取最近的任务(队列中是根据下次执行时间进行排序)去执行,如果时间没到则wait()方法等待。
而ScheduledThreadPoolExecutor
的执行步骤是,执行时向队列中添加一条任务,队列内部根据执行时间顺序进行了排序。然后线程池中的线程来获取要执行的任务,如果任务还没到执行时间就在这等,等到任务可以执行,然后获取到ScheduledFutureTask
执行,执行后修改下次的执行时间,再添加到队列中去。
ScheduledThreadPoolExecutor的运行机制
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定义,继承关系如下:
ThreadPoolExecutor:ExecutorService的实现类,其构造函数提供了灵活的参数配置,可构造多种类型的线程池
ScheduledThreadPoolExecutor:ScheduledExecutorService的实现类,用于任务调度
spring task对定时任务的两个抽象:
- TaskExecutor:与jdk中Executor相同,引入的目的是为定时任务的执行提供线程池的支持,如果设置,默认只有一个线程。
- TaskScheduler:提供定时任务支持,需要传入一个Runnable的任务做为参数,并指定需要周期执行的时间或者触发器,这样
Runnable
任务就可以周期性执行了。
继承关系如下:
任务执行器与调度器的实现类分别为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)
3.quartz
原理参考这篇文章: