简介

文章中代码案例已经同步到码云:代码中的schedule-demo中。

定时任务是指调度程序在指定的时间或周期触发执行的任务
使用场景:发送邮件、统计、状态修改、消息推送、活动开启、增量索引

现有的定时任务技术

  1. Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。使用较少。(不推荐使用,代码案例中已经给出说明)
  2. Spring3.0以后自主开发的定时任务工具spring task,使用简单,支持线程池,可以高效处理许多不同的定时任务,除spring相关的包外不需要额外的包,支持注解和配置文件两种形式。 不能处理过于复杂的任务
  3. 专业的定时框架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中的一些基本概念:

  1. Scheduler:任务调度器,是实际执行任务调度的控制器。在spring中通过SchedulerFactoryBean封装起来。
  2. 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个时间单元触发,时间单元可以是毫秒,秒,分,小时,日,月,年。
  3. Calendar:它是一些日历特定时间点的集合。一个trigger可以包含多个Calendar,以便排除或包含某些时间点。
  4. JobDetail:用来描述Job实现类及其它相关的静态信息,如Job名字、关联监听器等信息。在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用。
  5. 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)

源码解析

3.quartz

原理参考这篇文章:

quartz原理

参考

为什么使用ScheduledThreaedPoolExcutor而不是Timer

quartz原理解析

quartz原理


njitzyd
58 声望7 粉丝