天天看點

Java中定時任務Timer、Spring Task、quartz詳解

簡介

文章中代碼案例已經同步到碼雲:代碼中的

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被執行:

Java中定時任務Timer、Spring Task、quartz詳解

實作原理

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定義,繼承關系如下:

Java中定時任務Timer、Spring Task、quartz詳解

**ThreadPoolExecutor:**ExecutorService的實作類,其構造函數提供了靈活的參數配置,可構造多種類型的線程池

**ScheduledThreadPoolExecutor:**ScheduledExecutorService的實作類,用于任務排程

spring task對定時任務的兩個抽象:

  • TaskExecutor:與jdk中Executor相同,引入的目的是為定時任務的執行提供線程池的支援,如果設定**,預設隻有一個線程。**
  • TaskScheduler:提供定時任務支援,需要傳入一個Runnable的任務做為參數,并指定需要周期執行的時間或者觸發器,這樣

    Runnable

    任務就可以周期性執行了。

繼承關系如下:

Java中定時任務Timer、Spring Task、quartz詳解

任務執行器與排程器的實作類分别為ThreadPoolTaskExecutor、ThreadPoolTaskScheduler

TaskScheduler需要傳入一個Runnable的任務做為參數,并指定需要周期執行的時間或者觸發器(

Trigger

)。

spring定義了Trigger接口的實作類CronTrigger,支援使用cron表達式指定定時政策,使用如下:

在springboot項目中,我們一般都是使用

@schedule

注解來使用spring task,這個注解内部的實作就是使用上面的内容。

spring在初始化bean後,通過

postProcessAfterInitialization

攔截到所有的用到

@Scheduled

注解的方法,并解析相應的的注解參數,放入“定時任務清單”等待後續處理;之後再“定時任務清單”中統一執行相應的定時任務(任務為順序執行,先執行cron,之後再執行fixedRate)

源碼解析

3.quartz

原理參考這篇文章:

quartz原理

參考

為什麼使用ScheduledThreaedPoolExcutor而不是Timer

quartz原了解析

待後續處理;之後再“定時任務清單”中統一執行相應的定時任務(任務為順序執行,先執行cron,之後再執行fixedRate)

源碼解析

3.quartz

原理參考這篇文章:

quartz原理

參考

為什麼使用ScheduledThreaedPoolExcutor而不是Timer

quartz原了解析

quartz原理

繼續閱讀