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