Java中實現定時任務的兩種方法舉例詳解
一、定時任務
概念
定時任務是一種自動化執(zhí)行特定操作的方式,可以根據預定的時間、日期或間隔周期性地執(zhí)行某些任務。
在平常的生活中,大家肯定是有設置鬧鐘的習慣,我們需要通過鬧鐘來提醒我們到這個時刻,我們應該做指定的事情。同樣的在編程當中,我們很多時候也是需要實現這樣的操作的,到達指定的時刻,我們想要我們的程序去執(zhí)行某一個事情,比如:指定時間發(fā)送郵箱、指定時間發(fā)送生日祝福……
以上的種種到達指定時間做指定事情,就是定時任務。
作用
- 自動化任務執(zhí)行:定時任務能夠在預定的時間觸發(fā)執(zhí)行某些任務,無需人工干預。這對于需要定期執(zhí)行的重復性任務非常有效,例如數據備份、統(tǒng)計報表生成、系統(tǒng)維護等。
- 提高效率和準確性:通過定時任務,可以在特定的時間段內自動執(zhí)行任務,避免了人工操作的疏忽和錯誤。這樣可以提高任務的執(zhí)行效率和準確性,并降低因人為原因導致的錯誤風險。
- 節(jié)省時間和資源:定時任務可以代替人工手動執(zhí)行的操作,節(jié)省了大量人力資源和時間成本。同時,它也可以合理分配系統(tǒng)資源,避免任務集中導致的系統(tǒng)負載過高。
- 異步執(zhí)行:定時任務可以在后臺異步執(zhí)行,不會阻塞用戶的其他操作。這對于需要執(zhí)行耗時較長的任務或需要長時間運行的操作非常有用,可以提高系統(tǒng)的響應速度和用戶體驗。
二、簡單定時任務實現方式
今天我們來討論一下在Java中如何實現定時任務。定時任務在很多場景下都非常有用,例如定期執(zhí)行清理工作、數據備份、發(fā)送通知等。
在Java中,常見的可以實現定時任務的方式有如下幾種:
(1)線程類實現定時任務:比如Thread、Runnable、Callable等線程類都可以實現定時任務。
(2)Timer/TimerTask:Java提供了java.util.Timer和java.util.TimerTask類,可以用于創(chuàng)建定時任務。通過創(chuàng)建一個Timer對象,并調用其schedule()方法,可以指定任務的執(zhí)行時間和執(zhí)行間隔。然后,創(chuàng)建一個繼承自TimerTask的子類,實現具體的任務邏輯,并在run()方法中定義需要執(zhí)行的代碼。最后,將該任務對象通過Timer的schedule()方法進行調度即可。
(3)ScheduledExecutorService:Java提供了java.util.concurrent.ScheduledExecutorService接口,可以用于創(chuàng)建定時任務。通過調用ScheduledExecutorService的scheduleAtFixedRate()或scheduleWithFixedDelay()方法,可以指定任務的執(zhí)行時間和執(zhí)行間隔。然后,創(chuàng)建一個實現了Runnable接口的類,實現具體的任務邏輯,并在run()方法中定義需要執(zhí)行的代碼。最后,將該任務對象提交給ScheduledExecutorService進行調度即可。
(4)@Scheduled注解:這個是Spring框架所提供的,通過在方法上添加@Scheduled注解,并設置相應的時間表達式,就可以讓方法按照指定的時間間隔自動執(zhí)行。
1. Thread線程等待(最原始最簡單方式)
創(chuàng)建一個thread
,然后讓它在while
循環(huán)里一直運行著,通過sleep
方法來達到定時任務的效果。
/** * 匿名內部類實現 java.lang.Runnable 接口 */ public class ThreadTask { public static void main(String[] args) { final long timeInterval = 1000; //創(chuàng)建線程(匿名內部類方式) Thread thread = new Thread(new Runnable() { @Override public void run() { while (true){ SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); String dateStr = sdf.format(new Date()); System.out.println("線程等待實現定時任務:" + dateStr); try { Thread.sleep(timeInterval); } catch (InterruptedException e) { e.printStackTrace(); } } } }); //開啟線程 thread.start(); } }
public class ThreadTask1 { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); //創(chuàng)建線程(自定義類MyRunnable實現java.lang.Runnable接口) Thread t = new Thread(runnable); //開啟線程 t.start(); } } /** * 自定義類MyRunnable實現java.lang.Runnable接口 */ class MyRunnable implements Runnable{ final long timeInterval = 1000; @Override public void run() { while (true){ SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); String dateStr = sdf.format(new Date()); System.out.println("線程等待實現定時任務1:" + dateStr); try { Thread.sleep(timeInterval); } catch (InterruptedException e) { e.printStackTrace(); } } } }
2. 使用java.util.Timer
JDK
自帶的Timer API
算是最古老的定時任務實現方式了。Timer
是一種定時器工具,使用java.util.Timer
工具類。用來在一個后臺線程計劃執(zhí)行指定任務。它可以安排任務“執(zhí)行一次”或者定期“執(zhí)行多次”。
Timer類核心方法如下:
// 在指定延遲時間后執(zhí)行指定的任務 schedule(TimerTask task,long delay); // 在指定時間執(zhí)行指定的任務。(只執(zhí)行一次) schedule(TimerTask task, Date time); // 延遲指定時間(delay)之后,開始以指定的間隔(period)重復執(zhí)行指定的任務 schedule(TimerTask task,long delay,long period); // 在指定的時間開始按照指定的間隔(period)重復執(zhí)行指定的任務 schedule(TimerTask task, Date firstTime , long period); // 在指定的時間開始進行重復的固定速率執(zhí)行任務 scheduleAtFixedRate(TimerTask task,Date firstTime,long period); // 在指定的延遲后開始進行重復的固定速率執(zhí)行任務 scheduleAtFixedRate(TimerTask task,long delay,long period); // 終止此計時器,丟棄所有當前已安排的任務。 cancal(); // 從此計時器的任務隊列中移除所有已取消的任務。 purge();
import java.util.Timer; import java.util.TimerTask; public class TimerExample { public static void main(String[] args) { TimerTask task = new TimerTask() { @Override public void run() { System.out.println("Task executed at: " + System.currentTimeMillis()); } }; Timer timer = new Timer(); // 安排任務在1秒后執(zhí)行,并且每隔1秒執(zhí)行一次 timer.scheduleAtFixedRate(task, 1000, 1000); } }
在這個示例中,我們創(chuàng)建了一個Timer
對象,并用scheduleAtFixedRate
方法安排一個TimerTask
在1秒后開始執(zhí)行,并且每隔1秒執(zhí)行一次。
Timer 優(yōu)缺點分析
優(yōu)點:JDK自帶的,簡單易用。
缺點:
(1)對系統(tǒng)時間敏感
Timer類的任務調度是基于絕對時間的,而不是相對時間,所以它對系統(tǒng)時間的改變非常敏感。當系統(tǒng)時間發(fā)生變化時,可能導致任務執(zhí)行時間的誤差。
(2)不適合高并發(fā)場景
由于Timer類使用單個線程執(zhí)行所有任務,不適合在高并發(fā)環(huán)境下使用。當任務過多或任務執(zhí)行時間較長時,會影響整體性能和響應性。
(3)任務的無法持久化
當應用程序關閉或重啟時,Timer
中已經調度的任務會丟失。
(4)單線程執(zhí)行
Timer類內部使用單個線程來執(zhí)行所有的定時任務。如果某個任務執(zhí)行時間過長,會影響其他任務的執(zhí)行,可能導致任務被延遲。
當一個任務的執(zhí)行時間過長時,會影響其他任務的調度。
import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; /** * @author water * @date 2024/10/5 */ public class Main { public static void main(String[] args) { // 定時任務1 TimerTask timerTask = new TimerTask() { @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); String dateStr = sdf.format(new Date()); System.out.println("進入定時任務1:" + dateStr); // 休眠5秒 try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();} dateStr = sdf.format(new Date()); System.out.println("運行定時任務1:" + dateStr); } }; // 定時任務2 TimerTask timerTask2 = new TimerTask() { @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); String dateStr = sdf.format(new Date()); System.out.println("-----進入定時任務2:" + dateStr); dateStr = sdf.format(new Date()); System.out.println("-----運行定時任務2:" + dateStr); } }; // 計時器 Timer timer = new Timer(); // 添加執(zhí)行任務(延遲 1s 執(zhí)行,每 2s 執(zhí)行一次) timer.schedule(timerTask, 1000, 2000); timer.schedule(timerTask2, 1000, 2000); } }
這段代碼展示了如何使用Java的Timer
和TimerTask
類來實現定時任務的調度。以下是對代碼的分析:
將timerTask
安排在延遲1秒后執(zhí)行,隨后每2秒執(zhí)行一次。 將timerTask2
也安排在延遲1秒后執(zhí)行,隨后每2秒執(zhí)行一次。
定時任務1第一次運行時會在1秒后進入并輸出時間。由于在run()
方法中調用了sleep(3)
,這意味著此任務在執(zhí)行期間會阻塞3秒。這會導致timerTask
的后續(xù)執(zhí)行被延遲。
定時任務2將在1秒后運行,并每2秒執(zhí)行一次,但由于定時任務1在運行時阻塞了線程,可能會影響任務2的執(zhí)行頻率。
代碼的執(zhí)行結果如下,
任務調度的具體過程:
- 剛開始主程序啟動。
- 在時間是22:14:23.108時,任務1
timerTask
第一次執(zhí)行,打印“進入定時任務1”字符串。任務2也被調度開始執(zhí)行,但由于是單線程,任務2必須等待任務1完成。 - 在時間22:14:23.108到22:14:28.115時,任務1繼續(xù)執(zhí)行, 并休眠5秒,打印“運行定時任務1”字符串。此時任務2還是處于等待狀態(tài)。
- 在時間是22:14:28.115時,任務1完成。然后此時任務2就開始執(zhí)行,打印“進入定時任務2”和“運行定時任務2”字符串。
- 在時間是22:14:28.116時,因為初始的執(zhí)行間隔為2秒,所以任務1再次被調度,打印“進入定時任務1”字符串。但由于被調度再次執(zhí)行的任務1仍在執(zhí)行,任務2再次處于等待狀態(tài)。
- 在時間是22:14:28.116到22:14:33.110時,任務1繼續(xù)執(zhí)行, 并休眠5秒,打印“運行定時任務1”字符串。
- .....
當任務 1 運行時間超過設定的間隔時間時,任務 2 也會延遲執(zhí)行。 原本任務 1 和任務 2 的執(zhí)行時間間隔都是 2s,但因為任務 1 執(zhí)行了 5s,因此任務 2 的執(zhí)行時間間隔也變成了10秒(和原定時間不符)。
(5)錯誤處理能力有限
Timer線程是不會捕獲異常的,如果TimerTask拋出的了未檢查異常則會導致Timer線程終止,同時Timer也不會重新恢復線程的執(zhí)行,它會錯誤的認為整個Timer線程都會取消。同時,已經被安排單尚未執(zhí)行的TimerTask也不會再執(zhí)行了,新的任務也不能被調度。因此如果TimerTask拋出未檢查的異常,Timer將會產生無法預料的行為。
(6)任務異常影響其他任務
使用 Timer 類實現定時任務時,當一個任務拋出異常,其他任務也會終止運行。
Timer線程是不會捕獲異常的,如果TimerTask拋出的了未檢查異常則會導致Timer線程終止,同時Timer也不會重新恢復線程的執(zhí)行,它會錯誤的認為整個Timer線程都會取消。同時,已經被安排單尚未執(zhí)行的TimerTask也不會再執(zhí)行了,新的任務也不能被調度。
import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; /** * @author water * @date 2024/10/5 */ public class Main { public static void main(String[] args) { // 定時任務1 TimerTask timerTask = new TimerTask() { @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); String dateStr = sdf.format(new Date()); System.out.println("進入定時任務1:" + dateStr); //發(fā)生異常 int num = 10 / 0; dateStr = sdf.format(new Date()); System.out.println("運行定時任務1:" + dateStr); } }; // 定時任務2 TimerTask timerTask2 = new TimerTask() { @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); String dateStr = sdf.format(new Date()); System.out.println("----進入定時任務2:" + dateStr); dateStr = sdf.format(new Date()); System.out.println("----運行定時任務2:" + dateStr); } }; // 計時器 Timer timer = new Timer(); // 添加執(zhí)行任務(延遲 1s 執(zhí)行,每 2s 執(zhí)行一次) timer.schedule(timerTask, 1000, 2000); timer.schedule(timerTask2, 1000, 2000); } }
代碼的執(zhí)行結果如下,
3. 使用JDK自帶的ScheduledExecutorService
ScheduledExecutorService
是Java并發(fā)包(java.util.concurrent
)中的一個接口, 是JAVA 1.5后新增的定時任務接口,它是基于線程池設計的定時任務類,每個調度任務都會分配到線程池中的一個線程去執(zhí)行(任務是并發(fā)執(zhí)行,互不影響)。
ScheduledExecutorService
可以實現Timer具備的所有功能,并解決了 Timer類存在的問題提供了比Timer
更強大的定時任務調度功能。它可以調度任務在給定的延遲后運行,或者周期性地執(zhí)行。
注意:只有當執(zhí)行調度任務時,ScheduledExecutorService
才會真正啟動一個線程,其余時間ScheduledExecutorService
都是出于輪詢任務的狀態(tài)。
import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * @author water * @date 2024/10/5 */ public class Main { public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Runnable task = new Runnable() { @Override public void run() { String dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); System.out.println("執(zhí)行任務的時間:" + dateTime); } }; // 安排任務在1秒后執(zhí)行,并且每隔1秒執(zhí)行一次 scheduler.scheduleAtFixedRate(task, 1, 1, TimeUnit.SECONDS); } }
在這個示例中,我們創(chuàng)建了一個ScheduledExecutorService對象,并用scheduleAtFixedRate方法安排一個任務在1秒后開始執(zhí)行,并且每隔1秒執(zhí)行一次。
schedule和scheduleAtFixedRate的區(qū)別
在了解schedule與scheduleAtFixedRate方法的區(qū)別之前,先看看它們的相同點:
任務執(zhí)行未超時,下次執(zhí)行時間 = 上次執(zhí)行開始時間 + period。
任務執(zhí)行超時,下次執(zhí)行時間 = 上次執(zhí)行結束時間。
在任務執(zhí)行未超時時,它們都是上次執(zhí)行時間加上間隔時間,來執(zhí)行下一次任務。而執(zhí)行超時時,都是立馬執(zhí)行。
它們的不同點在于側重點不同
- schedule方法側重保持間隔時間的穩(wěn)定。
- scheduleAtFixedRate方法更加側重于保持執(zhí)行頻率的穩(wěn)定。
schedule側重保持間隔時間的穩(wěn)定
schedule
是固定延遲,更加側重保持延遲間隔的固定性。每次都是以上一個任務的起始時間來判斷時間間隔。
schedule方法會因為前一個任務的延遲而導致其后面的定時任務延時。計算公式為scheduledExecutionTime(第n+1次) = realExecutionTime(第n次) + periodTime。
也就是說如果第n次執(zhí)行task時,由于某種原因這次執(zhí)行時間過長,執(zhí)行完后的systemCurrentTime>= scheduledExecutionTime(第n+1次),則此時不做時隔等待,立即執(zhí)行第n+1次task。
而接下來的第n+2次task的scheduledExecutionTime(第n+2次)就隨著變成了realExecutionTime(第n+1次)+periodTime。這個方法更注重保持間隔時間的穩(wěn)定。
// 延遲1s后開始執(zhí)行任務,然后每隔2秒執(zhí)行 timer.schedule(task, 1000, 2000);
- 第0~1秒,等待狀態(tài);
- 第1秒,第一個任務開始執(zhí)行,執(zhí)行耗時3秒;
- 計算第二個任務的預定執(zhí)行時間:第一個任務的起始執(zhí)行時間 + 任務執(zhí)行周期兩秒鐘 = 1+2=3,所以第3秒是第二個任務的預定執(zhí)行時間;
- 第4秒,第一個任務執(zhí)行完畢,但是發(fā)現當前時間已經超過了第二個任務的預定執(zhí)行時間,所以第二個任務立即執(zhí)行,第二個任務的執(zhí)行時間是1秒鐘;
- 計算第三個任務的預定執(zhí)行時間:第二個任務起始執(zhí)行時間+任務執(zhí)行周期兩秒鐘=4+2=6,所以第三個任務是預定在第6秒執(zhí)行;
- 第5秒鐘,第二個任務執(zhí)行完畢,發(fā)現當前是第5秒,還未到第6秒,所以還需要等待1秒鐘。
scheduleAtFixedRate保持執(zhí)行頻率的穩(wěn)定
scheduleAtFixedRate
是固定速率,更加側重保持執(zhí)行頻率的穩(wěn)定性。scheduleAtFixedRate當前任務到達規(guī)定時間一定執(zhí)行,上一個未執(zhí)行的任務會直接終止。
scheduleAtFixedRate在反復執(zhí)行一個task的計劃時,每一次執(zhí)行這個task的計劃執(zhí)行時間在最初就被定下來了,也就是scheduledExecutionTime(第n次)=firstExecuteTime +n*periodTime。
如果第n次執(zhí)行task時,由于某種原因這次執(zhí)行時間過長,執(zhí)行完后的systemCurrentTime>= scheduledExecutionTime(第n+1次),則此時不做period間隔等待,立即執(zhí)行第n+1次task。
接下來的第n+2次的task的scheduledExecutionTime(第n+2次)依然還是firstExecuteTime+(n+2)*periodTime這在第一次執(zhí)行task就定下來了。說白了,這個方法更注重保持執(zhí)行頻率的穩(wěn)定。
如果用一句話來描述任務執(zhí)行超時之后schedule和scheduleAtFixedRate的區(qū)別就是:schedule的策略是錯過了就錯過了,后續(xù)按照新的節(jié)奏來走;scheduleAtFixedRate的策略是如果錯過了,就努力追上原來的節(jié)奏(制定好的節(jié)奏)。
簡而言之:schedule的策略是錯過了就錯過了,后續(xù)按照新的節(jié)奏來走;scheduleAtFixedRate的策略是如果錯過了,就努力追上原來的節(jié)奏。
4. 使用SpringTask實現定時任務
從Spring 3開始,Spring自帶了一套定時任務工具Spring-Task(基于注解 @Scheduled,@EnableScheduling 形式實現),可以把它看成是一個輕量級的Quartz,使用起來十分簡單,除Spring相關的包外不需要額外的包,支持注解和配置文件兩種形式。通常情況下在Spring體系內,針對簡單的定時任務,可直接使用Spring提供的功能。
如果你在使用Spring框架,可以利用@Scheduled注解來方便地實現定時任務。首先,需要確保你的Spring配置中啟用了任務調度功能。如果是在Spring Boot
項目中,需要在啟動類上添加@EnableScheduling
來開啟定時任務。
以 Spring Boot 為例,實現定時任務只需兩步:
- 開啟定時任務
- 添加定時任務
(1)開啟定時任務
如果是在Spring Boot項目中,需要在啟動類上添加@EnableScheduling來開啟定時任務
@EnableScheduling // 開啟定時任務 @SpringBootApplication public class Job4ScheduledApplication { public static void main(String[] args) { SpringApplication.run(Job4ScheduledApplication.class, args); } }
(2)添加定時任務
定時任務的添加只需要使用 @Scheduled 注解標注即可,如果有多個定時任務可以創(chuàng)建多個 @Scheduled 注解標注的方法。
@Component //@Component用于實例化類,將其類托管給 Spring 容器 public class TaskJobUtil { /** * cron表達式:表示每2秒 執(zhí)行任務 */ @Scheduled(cron = "0/2 * * * * ?") public void task() { System.out.println("task0-start"); sleep(5); System.out.println("task0-end"); } /** * fixedRate:每間隔2秒執(zhí)行一次任務 * 注意,默認情況下定時任務是在同一線程同步執(zhí)行的,如果任務的執(zhí)行時間(如5秒)大于間隔時間,則會等待任務執(zhí)行結束后直接開始下次任務 */ @Scheduled(fixedRate = 2000) public void task0() { System.out.println("task0-start"); sleep(5); System.out.println("task0-end"); } /** * fixedDelay:每次延時2秒執(zhí)行一次任務 * 注意,這里是等待上次任務執(zhí)行結束后,再延時固定時間后開始下次任務 */ @Scheduled(fixedDelay = 2000) public void task1() { System.out.println("task1-start"); sleep(5); System.out.println("task1-end"); } /** * initialDelay:首次任務啟動的延時時間 */ @Scheduled(initialDelay = 2000, fixedDelay = 3000) public void task2() { System.out.println("task2-start"); sleep(5); System.out.println("task2-end"); } private void sleep(long time) { try { TimeUnit.SECONDS.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } }
三、分布式定時任務實現方式
前面所有的定時任務,無論是基于線程類,還是基于 JDK 自帶的定時任務,還是基于Spring提供的Spring Task,都無法在分布式環(huán)境下使用,并且不支持持久化,一旦服務重啟所有的定時任務都將發(fā)生丟失,所以我們需要使用到其它的第三方成熟的定時任務框架。
1. Quartz
除了JDK自帶的API之外,我們還可以使用開源的框架來實現,比如Quartz。Quartz是一個開源的任務調度庫,用于在Java應用程序中實現定時任務調度和作業(yè)調度。,它允許開發(fā)者通過配置或編程方式定義、調度和管理任務。
使用Quartz可以開發(fā)一個或者多個定時任務,每個定時任務可以單獨指定執(zhí)行的時間,例如每隔1小時執(zhí)行一次、每個月第一天上午10點執(zhí)行一次、每個月最后一天下午5點執(zhí)行一次等。
Quartz既可以單獨使用也可以跟spring框架整合使用,在實際開發(fā)中一般會使用后者。
(1)Quartz的核心功能包括:
- 任務調度:定義任務的執(zhí)行計劃,并在指定時間或周期性執(zhí)行任務。
- 任務管理:管理和控制任務的生命周期,如啟動、暫停、刪除等。
- 持久化:支持將任務的狀態(tài)持久化到數據庫,以便在應用重啟后恢復任務狀態(tài)。
(2)Quartz架構圖如下:
Quartz主要由以下幾個核心組件組成:
- Scheduler:調度器,是Quartz的核心,用于管理和調度任務。
- Job:任務接口,定義任務的執(zhí)行邏輯,即具體要執(zhí)行的任務。所有Quartz任務必須實現這個接口。
- JobDetail:任務細節(jié)對象,定義了任務的具體實現和執(zhí)行參數。
- Trigger:觸發(fā)器,定義了任務的觸發(fā)條件,如時間、周期等。
- SimpleTrigger
- CronTirgger:和 Unix 的 cron 機制基本一樣,基于通用的公歷。
- DateIntervalTrigger
- NthIncludedDayTrigger
- JobDataMap:任務數據映射,用于傳遞任務執(zhí)行時所需的數據。
JobDetail就是對job的定義,而job是具體執(zhí)行的邏輯內容。 具體的執(zhí)行的邏輯需要實現 job類,并實現execute方法。如果使用JobDetail來定義,那么每次調度都會創(chuàng)建一個new job實例,這樣帶來的好處就是任務并發(fā)執(zhí)行的時候,互不干擾,不會對臨界資源造成影響。
(3)Quartz的使用步驟
使用Quartz進行定時任務調度通常包括以下步驟:
- 創(chuàng)建任務類:實現Job接口,定義任務的執(zhí)行邏輯。
- 配置調度器:創(chuàng)建并配置Scheduler實例。
- 定義任務細節(jié):創(chuàng)建JobDetail對象,指定任務類及其參數。
- 定義觸發(fā)器:創(chuàng)建Trigger對象,指定任務的觸發(fā)條件。
- 啟動調度器:將任務細節(jié)和觸發(fā)器注冊到調度器,并啟動調度器。
示例:使用Quartz進行定時任務調度
以下是一個使用Quartz進行定時任務調度的完整示例:
(1)創(chuàng)建任務類
在這個示例中,HelloJob
類實現了Job
接口,定義了任務的執(zhí)行邏輯,即打印一條消息。
import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class HelloJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("Hello, Quartz!"); } }
HelloJob 類該類實現了 Job 接口。實現了Quartz 調度器調用的核心方法 execute 方法。
execute 方法的JobExecutionContext context 參數允許作業(yè)訪問調度上下文中的信息,如觸發(fā)器、調度器等。在方法體內,使用 System.out.println("Hello, Quartz!"); 打印一條簡單的消息,表示作業(yè)被執(zhí)行。
(2)配置調度器
在這個示例中,我們創(chuàng)建了一個調度器,并定義了一個任務和一個觸發(fā)器。任務HelloJob
每10秒執(zhí)行一次,并在控制臺上打印消息。
import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.JobDataMap; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.CronScheduleBuilder; import org.quartz.SimpleScheduleBuilder; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.CronScheduleBuilder; import org.quartz.SimpleScheduleBuilder; public class QuartzExample { public static void main(String[] args) { try { // 創(chuàng)建調度器工廠 SchedulerFactory schedulerFactory = new org.quartz.impl.StdSchedulerFactory(); Scheduler scheduler = schedulerFactory.getScheduler(); // 定義任務細節(jié) JobDetail jobDetail = JobBuilder.newJob(HelloJob.class) .withIdentity("myJob", "group1") .usingJobData("key", "value") // 傳遞任務數據 .build(); // 定義觸發(fā)器 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("myTrigger", "group1") .startNow() .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) // 每10秒執(zhí)行一次 .repeatForever()) .build(); // 將任務細節(jié)和觸發(fā)器注冊到調度器 scheduler.scheduleJob(jobDetail, trigger); // 啟動調度器 scheduler.start(); } catch (SchedulerException e) { e.printStackTrace(); } } }
在這個示例中,我們創(chuàng)建一個調度器工廠的實例schedulerFactory,使用默認的標準調度器工廠。從調度器工廠獲取一個調度器實例scheduler,用于安排和執(zhí)行任務。
然后,創(chuàng)建一個新的任務細節(jié),指定作業(yè)類為 HelloJob
。該類應該實現 org.quartz.Job
接口。為任務指定唯一的標識符,名稱為 "myJob"
,組名為 "group1"
。通過 JobDataMap
向任務傳遞參數,方便在作業(yè)執(zhí)行時使用。構建最終的 JobDetail
對象。
創(chuàng)建一個新的觸發(fā)器構建器實例,為觸發(fā)器指定唯一的標識符,名稱為 "myTrigger"
,組名為 "group1",
設置觸發(fā)器為立即開始執(zhí)行。使用簡單調度器定義觸發(fā)規(guī)則:設置觸發(fā)器每 10 秒執(zhí)行一次,并且使觸發(fā)器無限期重復執(zhí)行。構建最終的 Trigger
對象。
將任務和觸發(fā)器注冊到調度器中,使其能夠根據觸發(fā)器的調度規(guī)則執(zhí)行任務。
啟動調度器,使其開始調度任務。
(3)使用Cron表達式
Quartz支持使用Cron表達式來定義更復雜的觸發(fā)條件。Cron表達式是一種字符串格式,用于表示任務的觸發(fā)時間。以下是一個使用Cron表達式的示例:
Trigger cronTrigger = TriggerBuilder.newTrigger() .withIdentity("myCronTrigger", "group1") .withSchedule(CronScheduleBuilder.cronSchedule("0 0/5 * * * ?")) // 每5分鐘執(zhí)行一次 .build();
在這個示例中,創(chuàng)建了一個名為 "myCronTrigger"
的 Cron 觸發(fā)器,它每 5 分鐘觸發(fā)一次。Cron表達式"0 0/5 * * * ?"
表示任務將在每5分鐘的開始時刻執(zhí)行一次。
Quartz的持久化
Quartz支持將任務的狀態(tài)持久化到數據庫,以便在應用重啟后恢復任務狀態(tài)。要使用持久化功能,需要配置Quartz的持久化存儲。
(1)配置持久化存儲
在quartz.properties
文件中配置數據庫連接和持久化存儲,
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate org.quartz.jobStore.dataSource = myDS org.quartz.jobStore.tablePrefix = QRTZ_ org.quartz.jobStore.isClustered = true
還需要配置數據源myDS
,以便Quartz能夠連接到數據庫。
(2)數據庫表
Quartz提供了創(chuàng)建數據庫表的SQL腳本,可以在Quartz官網下載。執(zhí)行這些腳本將創(chuàng)建Quartz所需的表。
2. XXL-Job
XXL-Job是一個輕量級分布式任務調度平臺。特點是平臺化,易部署,開發(fā)迅速、學習簡單、輕量級、易擴展。由調度中心和執(zhí)行器功能完成定時任務的執(zhí)行。調度中心負責統(tǒng)一調度,執(zhí)行器負責接收調度并執(zhí)行。
3. Elastic-Job
Elastic-Job是一個開源的分布式任務調度解決方案,它是基于Java的輕量級分布式調度框架。
比較
三者的比較
- 功能和特性:
- Quartz:Quartz是一個功能強大的作業(yè)調度框架,支持靈活的任務調度策略、分布式集群、任務持久化等特性。它具有豐富的API和擴展點,可以根據需求進行定制開發(fā)和擴展。
- XXL-Job:XXL-Job是一個分布式任務調度平臺,提供了可視化操作界面、多種任務調度方式、分片任務支持等特性。它注重于任務的管理和監(jiān)控,并提供了報警與告警功能。
- Elastic-Job:Elastic-Job是一個輕量級的分布式任務調度解決方案,支持分布式任務調度、彈性擴縮容、任務監(jiān)控和管理等特性。它注重于任務的彈性擴展和容錯機制。
- 分布式支持:
- Quartz:Quartz在分布式場景中需要基于數據庫鎖來保證操作的唯一性,通過多個節(jié)點的異步運行實現高可用性。但它沒有執(zhí)行層面的任務分片機制。
- XXL-Job:XXL-Job提供了分布式集群的支持,可以實現任務的負載均衡和高可用性。它支持分片任務和動態(tài)調整任務節(jié)點數量的特性。
- Elastic-Job:Elastic-Job支持分布式任務調度,具備彈性擴縮容能力,可以根據任務的執(zhí)行情況動態(tài)調整任務節(jié)點數量。
- 可視化和管理界面:
- Quartz:Quartz本身沒有提供可視化的任務管理界面,需要通過其他工具或自行開發(fā)來實現。
- XXL-Job:XXL-Job提供了簡潔直觀的任務管理界面,方便用戶進行任務的創(chuàng)建、編輯、狀態(tài)查看等操作。
- Elastic-Job:Elastic-Job提供了任務監(jiān)控和管理功能,可以查看任務的執(zhí)行日志、運行狀態(tài)、統(tǒng)計信息等。
- 社區(qū)活躍度和生態(tài)系統(tǒng):
- Quartz:Quartz是一個非常成熟且廣泛使用的作業(yè)調度框架,擁有強大的社區(qū)支持和豐富的生態(tài)系統(tǒng)。
- XXL-Job:XXL-Job也有一個活躍的社區(qū),并且在國內得到廣泛應用和認可。
- Elastic-Job:Elastic-Job相對較新,并且社區(qū)規(guī)模較小,但其在分布式任務調度領域有一定的影響力。
- 應用場景:
- Quartz在功能和擴展性上非常強大,適用于復雜的任務調度需求。
- XXL-Job注重于任務管理和監(jiān)控,并提供了可視化的操作界面。
- Elastic-Job輕量級且具備分布式任務調度和彈性擴縮容能力。
四、總結
(1)線程+休眠實現定時任務,是最簡單實現定時任務的方式了,但這只是提供一種思路,實習開發(fā)中幾乎不會使用。
(2)JDK自帶的定時任務Timer和ScheduledExecutorService,我們需要了解兩者的區(qū)別。
- Timer是單線程的,一旦發(fā)生異常,將終止所有的任務;Timer是絕對時間的,會受到系統(tǒng)時間的影響。
- ScheduledExecutorService是基于線程池,是多線程的,一旦發(fā)生異常,不會終止所有的任務;ScheduledExecutorService是相對時間 ,不會受到系統(tǒng)時間的影響。
- 注意區(qū)固定間隔和固定頻率的區(qū)別。
(3)Spring Task實現的定時任務是基于線程池,是多線程的,一旦發(fā)生異常,不會終止所有的任務;基于相對時間,不會受到系統(tǒng)時間的影響。
(4)分布式定時任務,一般是直接使用第三方成熟的定時任務框架,當然如果你公司資金充足可以選擇開發(fā)定制化定時任務框架。選用開源的第三方成熟定時任務框架,好處在于功能完善、免費,代碼質量也是有保障的。
如果你當前系統(tǒng)比較小,或者說沒那么在意可靠性,可以選用 JDK自帶的定時任務或者是SpringTask,否則就選用分布式定時任務框架,輕量級就可以選用 XXL-Job,大型系統(tǒng)可以選用Quartz。
到此這篇關于Java中實現定時任務的兩種方法的文章就介紹到這了,更多相關Java實現定時任務內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringCloud通過Feign傳遞List類型參數方式
這篇文章主要介紹了SpringCloud通過Feign傳遞List類型參數方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03Java并發(fā)編程:CountDownLatch與CyclicBarrier和Semaphore的實例詳解
這篇文章主要介紹了Java并發(fā)編程:CountDownLatch與CyclicBarrier和Semaphore的實例詳解的相關資料,需要的朋友可以參考下2017-09-09Springboot+ElementUi實現評論、回復、點贊功能
這篇文章主要介紹了通過Springboot ElementUi實現評論、回復、點贊功能。如果是自己評論的還可以刪除,刪除的規(guī)則是如果該評論下還有回復,也一并刪除。需要的可以參考一下2022-01-01