簡介
文章中代碼案例已經同步到碼雲:代碼中的
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配置:
<!--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被執行:

實作原理
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定義,繼承關系如下:
**ThreadPoolExecutor:**ExecutorService的實作類,其構造函數提供了靈活的參數配置,可構造多種類型的線程池
**ScheduledThreadPoolExecutor:**ScheduledExecutorService的實作類,用于任務排程
spring task對定時任務的兩個抽象:
- TaskExecutor:與jdk中Executor相同,引入的目的是為定時任務的執行提供線程池的支援,如果設定**,預設隻有一個線程。**
- TaskScheduler:提供定時任務支援,需要傳入一個Runnable的任務做為參數,并指定需要周期執行的時間或者觸發器,這樣
任務就可以周期性執行了。Runnable
繼承關系如下:
任務執行器與排程器的實作類分别為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原理