Java中實現(xiàn)定時任務(wù)的兩種方法舉例詳解
一、定時任務(wù)
概念
定時任務(wù)是一種自動化執(zhí)行特定操作的方式,可以根據(jù)預(yù)定的時間、日期或間隔周期性地執(zhí)行某些任務(wù)。
在平常的生活中,大家肯定是有設(shè)置鬧鐘的習(xí)慣,我們需要通過鬧鐘來提醒我們到這個時刻,我們應(yīng)該做指定的事情。同樣的在編程當(dāng)中,我們很多時候也是需要實現(xiàn)這樣的操作的,到達(dá)指定的時刻,我們想要我們的程序去執(zhí)行某一個事情,比如:指定時間發(fā)送郵箱、指定時間發(fā)送生日祝福……
以上的種種到達(dá)指定時間做指定事情,就是定時任務(wù)。
作用
- 自動化任務(wù)執(zhí)行:定時任務(wù)能夠在預(yù)定的時間觸發(fā)執(zhí)行某些任務(wù),無需人工干預(yù)。這對于需要定期執(zhí)行的重復(fù)性任務(wù)非常有效,例如數(shù)據(jù)備份、統(tǒng)計報表生成、系統(tǒng)維護(hù)等。
- 提高效率和準(zhǔn)確性:通過定時任務(wù),可以在特定的時間段內(nèi)自動執(zhí)行任務(wù),避免了人工操作的疏忽和錯誤。這樣可以提高任務(wù)的執(zhí)行效率和準(zhǔn)確性,并降低因人為原因?qū)е碌腻e誤風(fēng)險。
- 節(jié)省時間和資源:定時任務(wù)可以代替人工手動執(zhí)行的操作,節(jié)省了大量人力資源和時間成本。同時,它也可以合理分配系統(tǒng)資源,避免任務(wù)集中導(dǎo)致的系統(tǒng)負(fù)載過高。
- 異步執(zhí)行:定時任務(wù)可以在后臺異步執(zhí)行,不會阻塞用戶的其他操作。這對于需要執(zhí)行耗時較長的任務(wù)或需要長時間運行的操作非常有用,可以提高系統(tǒng)的響應(yīng)速度和用戶體驗。
二、簡單定時任務(wù)實現(xiàn)方式
今天我們來討論一下在Java中如何實現(xiàn)定時任務(wù)。定時任務(wù)在很多場景下都非常有用,例如定期執(zhí)行清理工作、數(shù)據(jù)備份、發(fā)送通知等。
在Java中,常見的可以實現(xiàn)定時任務(wù)的方式有如下幾種:
(1)線程類實現(xiàn)定時任務(wù):比如Thread、Runnable、Callable等線程類都可以實現(xiàn)定時任務(wù)。
(2)Timer/TimerTask:Java提供了java.util.Timer和java.util.TimerTask類,可以用于創(chuàng)建定時任務(wù)。通過創(chuàng)建一個Timer對象,并調(diào)用其schedule()方法,可以指定任務(wù)的執(zhí)行時間和執(zhí)行間隔。然后,創(chuàng)建一個繼承自TimerTask的子類,實現(xiàn)具體的任務(wù)邏輯,并在run()方法中定義需要執(zhí)行的代碼。最后,將該任務(wù)對象通過Timer的schedule()方法進(jìn)行調(diào)度即可。
(3)ScheduledExecutorService:Java提供了java.util.concurrent.ScheduledExecutorService接口,可以用于創(chuàng)建定時任務(wù)。通過調(diào)用ScheduledExecutorService的scheduleAtFixedRate()或scheduleWithFixedDelay()方法,可以指定任務(wù)的執(zhí)行時間和執(zhí)行間隔。然后,創(chuàng)建一個實現(xiàn)了Runnable接口的類,實現(xiàn)具體的任務(wù)邏輯,并在run()方法中定義需要執(zhí)行的代碼。最后,將該任務(wù)對象提交給ScheduledExecutorService進(jìn)行調(diào)度即可。
(4)@Scheduled注解:這個是Spring框架所提供的,通過在方法上添加@Scheduled注解,并設(shè)置相應(yīng)的時間表達(dá)式,就可以讓方法按照指定的時間間隔自動執(zhí)行。
1. Thread線程等待(最原始最簡單方式)
創(chuàng)建一個thread,然后讓它在while循環(huán)里一直運行著,通過sleep方法來達(dá)到定時任務(wù)的效果。
/**
* 匿名內(nèi)部類實現(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("線程等待實現(xiàn)定時任務(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實現(xiàn)java.lang.Runnable接口)
Thread t = new Thread(runnable);
//開啟線程
t.start();
}
}
/**
* 自定義類MyRunnable實現(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("線程等待實現(xiàn)定時任務(wù)1:" + dateStr);
try {
Thread.sleep(timeInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}2. 使用java.util.Timer
JDK自帶的Timer API算是最古老的定時任務(wù)實現(xiàn)方式了。Timer是一種定時器工具,使用java.util.Timer工具類。用來在一個后臺線程計劃執(zhí)行指定任務(wù)。它可以安排任務(wù)“執(zhí)行一次”或者定期“執(zhí)行多次”。
Timer類核心方法如下:
// 在指定延遲時間后執(zhí)行指定的任務(wù) schedule(TimerTask task,long delay); // 在指定時間執(zhí)行指定的任務(wù)。(只執(zhí)行一次) schedule(TimerTask task, Date time); // 延遲指定時間(delay)之后,開始以指定的間隔(period)重復(fù)執(zhí)行指定的任務(wù) schedule(TimerTask task,long delay,long period); // 在指定的時間開始按照指定的間隔(period)重復(fù)執(zhí)行指定的任務(wù) schedule(TimerTask task, Date firstTime , long period); // 在指定的時間開始進(jìn)行重復(fù)的固定速率執(zhí)行任務(wù) scheduleAtFixedRate(TimerTask task,Date firstTime,long period); // 在指定的延遲后開始進(jìn)行重復(fù)的固定速率執(zhí)行任務(wù) scheduleAtFixedRate(TimerTask task,long delay,long period); // 終止此計時器,丟棄所有當(dāng)前已安排的任務(wù)。 cancal(); // 從此計時器的任務(wù)隊列中移除所有已取消的任務(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);
}
}
在這個示例中,我們創(chuàng)建了一個Timer對象,并用scheduleAtFixedRate方法安排一個TimerTask在1秒后開始執(zhí)行,并且每隔1秒執(zhí)行一次。
Timer 優(yōu)缺點分析
優(yōu)點:JDK自帶的,簡單易用。
缺點:
(1)對系統(tǒng)時間敏感
Timer類的任務(wù)調(diào)度是基于絕對時間的,而不是相對時間,所以它對系統(tǒng)時間的改變非常敏感。當(dāng)系統(tǒng)時間發(fā)生變化時,可能導(dǎo)致任務(wù)執(zhí)行時間的誤差。
(2)不適合高并發(fā)場景
由于Timer類使用單個線程執(zhí)行所有任務(wù),不適合在高并發(fā)環(huán)境下使用。當(dāng)任務(wù)過多或任務(wù)執(zhí)行時間較長時,會影響整體性能和響應(yīng)性。
(3)任務(wù)的無法持久化
當(dāng)應(yīng)用程序關(guān)閉或重啟時,Timer 中已經(jīng)調(diào)度的任務(wù)會丟失。
(4)單線程執(zhí)行
Timer類內(nèi)部使用單個線程來執(zhí)行所有的定時任務(wù)。如果某個任務(wù)執(zhí)行時間過長,會影響其他任務(wù)的執(zhí)行,可能導(dǎo)致任務(wù)被延遲。
當(dāng)一個任務(wù)的執(zhí)行時間過長時,會影響其他任務(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) {
// 定時任務(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)入定時任務(wù)1:" + dateStr);
// 休眠5秒
try {TimeUnit.SECONDS.sleep(5);}
catch (InterruptedException e) {e.printStackTrace();}
dateStr = sdf.format(new Date());
System.out.println("運行定時任務(wù)1:" + dateStr);
}
};
// 定時任務(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)入定時任務(wù)2:" + dateStr);
dateStr = sdf.format(new Date());
System.out.println("-----運行定時任務(wù)2:" + dateStr);
}
};
// 計時器
Timer timer = new Timer();
// 添加執(zhí)行任務(wù)(延遲 1s 執(zhí)行,每 2s 執(zhí)行一次)
timer.schedule(timerTask, 1000, 2000);
timer.schedule(timerTask2, 1000, 2000);
}
}
這段代碼展示了如何使用Java的Timer和TimerTask類來實現(xiàn)定時任務(wù)的調(diào)度。以下是對代碼的分析:
將timerTask安排在延遲1秒后執(zhí)行,隨后每2秒執(zhí)行一次。 將timerTask2也安排在延遲1秒后執(zhí)行,隨后每2秒執(zhí)行一次。
定時任務(wù)1第一次運行時會在1秒后進(jìn)入并輸出時間。由于在run()方法中調(diào)用了sleep(3),這意味著此任務(wù)在執(zhí)行期間會阻塞3秒。這會導(dǎo)致timerTask的后續(xù)執(zhí)行被延遲。
定時任務(wù)2將在1秒后運行,并每2秒執(zhí)行一次,但由于定時任務(wù)1在運行時阻塞了線程,可能會影響任務(wù)2的執(zhí)行頻率。
代碼的執(zhí)行結(jié)果如下,

任務(wù)調(diào)度的具體過程:
- 剛開始主程序啟動。
- 在時間是22:14:23.108時,任務(wù)1
timerTask第一次執(zhí)行,打印“進(jìn)入定時任務(wù)1”字符串。任務(wù)2也被調(diào)度開始執(zhí)行,但由于是單線程,任務(wù)2必須等待任務(wù)1完成。 - 在時間22:14:23.108到22:14:28.115時,任務(wù)1繼續(xù)執(zhí)行, 并休眠5秒,打印“運行定時任務(wù)1”字符串。此時任務(wù)2還是處于等待狀態(tài)。
- 在時間是22:14:28.115時,任務(wù)1完成。然后此時任務(wù)2就開始執(zhí)行,打印“進(jìn)入定時任務(wù)2”和“運行定時任務(wù)2”字符串。
- 在時間是22:14:28.116時,因為初始的執(zhí)行間隔為2秒,所以任務(wù)1再次被調(diào)度,打印“進(jìn)入定時任務(wù)1”字符串。但由于被調(diào)度再次執(zhí)行的任務(wù)1仍在執(zhí)行,任務(wù)2再次處于等待狀態(tài)。
- 在時間是22:14:28.116到22:14:33.110時,任務(wù)1繼續(xù)執(zhí)行, 并休眠5秒,打印“運行定時任務(wù)1”字符串。
- .....
當(dāng)任務(wù) 1 運行時間超過設(shè)定的間隔時間時,任務(wù) 2 也會延遲執(zhí)行。 原本任務(wù) 1 和任務(wù) 2 的執(zhí)行時間間隔都是 2s,但因為任務(wù) 1 執(zhí)行了 5s,因此任務(wù) 2 的執(zhí)行時間間隔也變成了10秒(和原定時間不符)。
(5)錯誤處理能力有限
Timer線程是不會捕獲異常的,如果TimerTask拋出的了未檢查異常則會導(dǎo)致Timer線程終止,同時Timer也不會重新恢復(fù)線程的執(zhí)行,它會錯誤的認(rèn)為整個Timer線程都會取消。同時,已經(jīng)被安排單尚未執(zhí)行的TimerTask也不會再執(zhí)行了,新的任務(wù)也不能被調(diào)度。因此如果TimerTask拋出未檢查的異常,Timer將會產(chǎn)生無法預(yù)料的行為。
(6)任務(wù)異常影響其他任務(wù)
使用 Timer 類實現(xiàn)定時任務(wù)時,當(dāng)一個任務(wù)拋出異常,其他任務(wù)也會終止運行。
Timer線程是不會捕獲異常的,如果TimerTask拋出的了未檢查異常則會導(dǎo)致Timer線程終止,同時Timer也不會重新恢復(fù)線程的執(zhí)行,它會錯誤的認(rèn)為整個Timer線程都會取消。同時,已經(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) {
// 定時任務(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)入定時任務(wù)1:" + dateStr);
//發(fā)生異常
int num = 10 / 0;
dateStr = sdf.format(new Date());
System.out.println("運行定時任務(wù)1:" + dateStr);
}
};
// 定時任務(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)入定時任務(wù)2:" + dateStr);
dateStr = sdf.format(new Date());
System.out.println("----運行定時任務(wù)2:" + dateStr);
}
};
// 計時器
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)中的一個接口, 是JAVA 1.5后新增的定時任務(wù)接口,它是基于線程池設(shè)計的定時任務(wù)類,每個調(diào)度任務(wù)都會分配到線程池中的一個線程去執(zhí)行(任務(wù)是并發(fā)執(zhí)行,互不影響)。
ScheduledExecutorService可以實現(xiàn)Timer具備的所有功能,并解決了 Timer類存在的問題提供了比Timer更強(qiáng)大的定時任務(wù)調(diào)度功能。它可以調(diào)度任務(wù)在給定的延遲后運行,或者周期性地執(zhí)行。
注意:只有當(dāng)執(zhí)行調(diào)度任務(wù)時,ScheduledExecutorService才會真正啟動一個線程,其余時間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ù)的時間:" + dateTime);
}
};
// 安排任務(wù)在1秒后執(zhí)行,并且每隔1秒執(zhí)行一次
scheduler.scheduleAtFixedRate(task, 1, 1, TimeUnit.SECONDS);
}
}
在這個示例中,我們創(chuàng)建了一個ScheduledExecutorService對象,并用scheduleAtFixedRate方法安排一個任務(wù)在1秒后開始執(zhí)行,并且每隔1秒執(zhí)行一次。

schedule和scheduleAtFixedRate的區(qū)別
在了解schedule與scheduleAtFixedRate方法的區(qū)別之前,先看看它們的相同點:
任務(wù)執(zhí)行未超時,下次執(zhí)行時間 = 上次執(zhí)行開始時間 + period。
任務(wù)執(zhí)行超時,下次執(zhí)行時間 = 上次執(zhí)行結(jié)束時間。
在任務(wù)執(zhí)行未超時時,它們都是上次執(zhí)行時間加上間隔時間,來執(zhí)行下一次任務(wù)。而執(zhí)行超時時,都是立馬執(zhí)行。
它們的不同點在于側(cè)重點不同
- schedule方法側(cè)重保持間隔時間的穩(wěn)定。
- scheduleAtFixedRate方法更加側(cè)重于保持執(zhí)行頻率的穩(wěn)定。
schedule側(cè)重保持間隔時間的穩(wěn)定
schedule是固定延遲,更加側(cè)重保持延遲間隔的固定性。每次都是以上一個任務(wù)的起始時間來判斷時間間隔。
schedule方法會因為前一個任務(wù)的延遲而導(dǎo)致其后面的定時任務(wù)延時。計算公式為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í)行任務(wù),然后每隔2秒執(zhí)行 timer.schedule(task, 1000, 2000);
- 第0~1秒,等待狀態(tài);
- 第1秒,第一個任務(wù)開始執(zhí)行,執(zhí)行耗時3秒;
- 計算第二個任務(wù)的預(yù)定執(zhí)行時間:第一個任務(wù)的起始執(zhí)行時間 + 任務(wù)執(zhí)行周期兩秒鐘 = 1+2=3,所以第3秒是第二個任務(wù)的預(yù)定執(zhí)行時間;
- 第4秒,第一個任務(wù)執(zhí)行完畢,但是發(fā)現(xiàn)當(dāng)前時間已經(jīng)超過了第二個任務(wù)的預(yù)定執(zhí)行時間,所以第二個任務(wù)立即執(zhí)行,第二個任務(wù)的執(zhí)行時間是1秒鐘;
- 計算第三個任務(wù)的預(yù)定執(zhí)行時間:第二個任務(wù)起始執(zhí)行時間+任務(wù)執(zhí)行周期兩秒鐘=4+2=6,所以第三個任務(wù)是預(yù)定在第6秒執(zhí)行;
- 第5秒鐘,第二個任務(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ī)定時間一定執(zhí)行,上一個未執(zhí)行的任務(wù)會直接終止。
scheduleAtFixedRate在反復(fù)執(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)定。
如果用一句話來描述任務(wù)執(zhí)行超時之后schedule和scheduleAtFixedRate的區(qū)別就是:schedule的策略是錯過了就錯過了,后續(xù)按照新的節(jié)奏來走;scheduleAtFixedRate的策略是如果錯過了,就努力追上原來的節(jié)奏(制定好的節(jié)奏)。

簡而言之:schedule的策略是錯過了就錯過了,后續(xù)按照新的節(jié)奏來走;scheduleAtFixedRate的策略是如果錯過了,就努力追上原來的節(jié)奏。
4. 使用SpringTask實現(xiàn)定時任務(wù)
從Spring 3開始,Spring自帶了一套定時任務(wù)工具Spring-Task(基于注解 @Scheduled,@EnableScheduling 形式實現(xiàn)),可以把它看成是一個輕量級的Quartz,使用起來十分簡單,除Spring相關(guān)的包外不需要額外的包,支持注解和配置文件兩種形式。通常情況下在Spring體系內(nèi),針對簡單的定時任務(wù),可直接使用Spring提供的功能。
如果你在使用Spring框架,可以利用@Scheduled注解來方便地實現(xiàn)定時任務(wù)。首先,需要確保你的Spring配置中啟用了任務(wù)調(diào)度功能。如果是在Spring Boot項目中,需要在啟動類上添加@EnableScheduling來開啟定時任務(wù)。
以 Spring Boot 為例,實現(xiàn)定時任務(wù)只需兩步:
- 開啟定時任務(wù)
- 添加定時任務(wù)
(1)開啟定時任務(wù)
如果是在Spring Boot項目中,需要在啟動類上添加@EnableScheduling來開啟定時任務(wù)
@EnableScheduling // 開啟定時任務(wù)
@SpringBootApplication
public class Job4ScheduledApplication {
public static void main(String[] args) {
SpringApplication.run(Job4ScheduledApplication.class, args);
}
}(2)添加定時任務(wù)
定時任務(wù)的添加只需要使用 @Scheduled 注解標(biāo)注即可,如果有多個定時任務(wù)可以創(chuàng)建多個 @Scheduled 注解標(biāo)注的方法。
@Component //@Component用于實例化類,將其類托管給 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)情況下定時任務(wù)是在同一線程同步執(zhí)行的,如果任務(wù)的執(zhí)行時間(如5秒)大于間隔時間,則會等待任務(wù)執(zhí)行結(jié)束后直接開始下次任務(wù)
*/
@Scheduled(fixedRate = 2000)
public void task0() {
System.out.println("task0-start");
sleep(5);
System.out.println("task0-end");
}
/**
* fixedDelay:每次延時2秒執(zhí)行一次任務(wù)
* 注意,這里是等待上次任務(wù)執(zhí)行結(jié)束后,再延時固定時間后開始下次任務(wù)
*/
@Scheduled(fixedDelay = 2000)
public void task1() {
System.out.println("task1-start");
sleep(5);
System.out.println("task1-end");
}
/**
* initialDelay:首次任務(wù)啟動的延時時間
*/
@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();
}
}
}三、分布式定時任務(wù)實現(xiàn)方式
前面所有的定時任務(wù),無論是基于線程類,還是基于 JDK 自帶的定時任務(wù),還是基于Spring提供的Spring Task,都無法在分布式環(huán)境下使用,并且不支持持久化,一旦服務(wù)重啟所有的定時任務(wù)都將發(fā)生丟失,所以我們需要使用到其它的第三方成熟的定時任務(wù)框架。
1. Quartz
除了JDK自帶的API之外,我們還可以使用開源的框架來實現(xiàn),比如Quartz。Quartz是一個開源的任務(wù)調(diào)度庫,用于在Java應(yīng)用程序中實現(xiàn)定時任務(wù)調(diào)度和作業(yè)調(diào)度。,它允許開發(fā)者通過配置或編程方式定義、調(diào)度和管理任務(wù)。
使用Quartz可以開發(fā)一個或者多個定時任務(wù),每個定時任務(wù)可以單獨指定執(zhí)行的時間,例如每隔1小時執(zhí)行一次、每個月第一天上午10點執(zhí)行一次、每個月最后一天下午5點執(zhí)行一次等。
Quartz既可以單獨使用也可以跟spring框架整合使用,在實際開發(fā)中一般會使用后者。
(1)Quartz的核心功能包括:
- 任務(wù)調(diào)度:定義任務(wù)的執(zhí)行計劃,并在指定時間或周期性執(zhí)行任務(wù)。
- 任務(wù)管理:管理和控制任務(wù)的生命周期,如啟動、暫停、刪除等。
- 持久化:支持將任務(wù)的狀態(tài)持久化到數(shù)據(jù)庫,以便在應(yīng)用重啟后恢復(fù)任務(wù)狀態(tài)。
(2)Quartz架構(gòu)圖如下:

Quartz主要由以下幾個核心組件組成:
- Scheduler:調(diào)度器,是Quartz的核心,用于管理和調(diào)度任務(wù)。
- Job:任務(wù)接口,定義任務(wù)的執(zhí)行邏輯,即具體要執(zhí)行的任務(wù)。所有Quartz任務(wù)必須實現(xiàn)這個接口。
- JobDetail:任務(wù)細(xì)節(jié)對象,定義了任務(wù)的具體實現(xiàn)和執(zhí)行參數(shù)。
- Trigger:觸發(fā)器,定義了任務(wù)的觸發(fā)條件,如時間、周期等。
- SimpleTrigger
- CronTirgger:和 Unix 的 cron 機(jī)制基本一樣,基于通用的公歷。
- DateIntervalTrigger
- NthIncludedDayTrigger
- JobDataMap:任務(wù)數(shù)據(jù)映射,用于傳遞任務(wù)執(zhí)行時所需的數(shù)據(jù)。
JobDetail就是對job的定義,而job是具體執(zhí)行的邏輯內(nèi)容。 具體的執(zhí)行的邏輯需要實現(xiàn) job類,并實現(xiàn)execute方法。如果使用JobDetail來定義,那么每次調(diào)度都會創(chuàng)建一個new job實例,這樣帶來的好處就是任務(wù)并發(fā)執(zhí)行的時候,互不干擾,不會對臨界資源造成影響。
(3)Quartz的使用步驟
使用Quartz進(jìn)行定時任務(wù)調(diào)度通常包括以下步驟:
- 創(chuàng)建任務(wù)類:實現(xiàn)Job接口,定義任務(wù)的執(zhí)行邏輯。
- 配置調(diào)度器:創(chuàng)建并配置Scheduler實例。
- 定義任務(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)行定時任務(wù)調(diào)度
以下是一個使用Quartz進(jìn)行定時任務(wù)調(diào)度的完整示例:
(1)創(chuàng)建任務(wù)類
在這個示例中,HelloJob類實現(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 類該類實現(xiàn)了 Job 接口。實現(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)度器
在這個示例中,我們創(chuàng)建了一個調(diào)度器,并定義了一個任務(wù)和一個觸發(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();
}
}
}
在這個示例中,我們創(chuàng)建一個調(diào)度器工廠的實例schedulerFactory,使用默認(rèn)的標(biāo)準(zhǔn)調(diào)度器工廠。從調(diào)度器工廠獲取一個調(diào)度器實例scheduler,用于安排和執(zhí)行任務(wù)。
然后,創(chuàng)建一個新的任務(wù)細(xì)節(jié),指定作業(yè)類為 HelloJob。該類應(yīng)該實現(xiàn) org.quartz.Job 接口。為任務(wù)指定唯一的標(biāo)識符,名稱為 "myJob",組名為 "group1"。通過 JobDataMap 向任務(wù)傳遞參數(shù),方便在作業(yè)執(zhí)行時使用。構(gòu)建最終的 JobDetail 對象。
創(chuàng)建一個新的觸發(fā)器構(gòu)建器實例,為觸發(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ā)時間。以下是一個使用Cron表達(dá)式的示例:
Trigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity("myCronTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/5 * * * ?")) // 每5分鐘執(zhí)行一次
.build();
在這個示例中,創(chuàng)建了一個名為 "myCronTrigger" 的 Cron 觸發(fā)器,它每 5 分鐘觸發(fā)一次。Cron表達(dá)式"0 0/5 * * * ?"表示任務(wù)將在每5分鐘的開始時刻執(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是一個輕量級分布式任務(wù)調(diào)度平臺。特點是平臺化,易部署,開發(fā)迅速、學(xué)習(xí)簡單、輕量級、易擴(kuò)展。由調(diào)度中心和執(zhí)行器功能完成定時任務(wù)的執(zhí)行。調(diào)度中心負(fù)責(zé)統(tǒng)一調(diào)度,執(zhí)行器負(fù)責(zé)接收調(diào)度并執(zhí)行。
3. Elastic-Job
Elastic-Job是一個開源的分布式任務(wù)調(diào)度解決方案,它是基于Java的輕量級分布式調(diào)度框架。
比較
三者的比較
- 功能和特性:
- Quartz:Quartz是一個功能強(qiáng)大的作業(yè)調(diào)度框架,支持靈活的任務(wù)調(diào)度策略、分布式集群、任務(wù)持久化等特性。它具有豐富的API和擴(kuò)展點,可以根據(jù)需求進(jìn)行定制開發(fā)和擴(kuò)展。
- XXL-Job:XXL-Job是一個分布式任務(wù)調(diào)度平臺,提供了可視化操作界面、多種任務(wù)調(diào)度方式、分片任務(wù)支持等特性。它注重于任務(wù)的管理和監(jiān)控,并提供了報警與告警功能。
- Elastic-Job:Elastic-Job是一個輕量級的分布式任務(wù)調(diào)度解決方案,支持分布式任務(wù)調(diào)度、彈性擴(kuò)縮容、任務(wù)監(jiān)控和管理等特性。它注重于任務(wù)的彈性擴(kuò)展和容錯機(jī)制。
- 分布式支持:
- Quartz:Quartz在分布式場景中需要基于數(shù)據(jù)庫鎖來保證操作的唯一性,通過多個節(jié)點的異步運行實現(xiàn)高可用性。但它沒有執(zhí)行層面的任務(wù)分片機(jī)制。
- XXL-Job:XXL-Job提供了分布式集群的支持,可以實現(xiàn)任務(wù)的負(fù)載均衡和高可用性。它支持分片任務(wù)和動態(tài)調(diào)整任務(wù)節(jié)點數(shù)量的特性。
- Elastic-Job:Elastic-Job支持分布式任務(wù)調(diào)度,具備彈性擴(kuò)縮容能力,可以根據(jù)任務(wù)的執(zhí)行情況動態(tài)調(diào)整任務(wù)節(jié)點數(shù)量。
- 可視化和管理界面:
- Quartz:Quartz本身沒有提供可視化的任務(wù)管理界面,需要通過其他工具或自行開發(fā)來實現(xiàn)。
- XXL-Job:XXL-Job提供了簡潔直觀的任務(wù)管理界面,方便用戶進(jìn)行任務(wù)的創(chuàng)建、編輯、狀態(tài)查看等操作。
- Elastic-Job:Elastic-Job提供了任務(wù)監(jiān)控和管理功能,可以查看任務(wù)的執(zhí)行日志、運行狀態(tài)、統(tǒng)計信息等。
- 社區(qū)活躍度和生態(tài)系統(tǒng):
- Quartz:Quartz是一個非常成熟且廣泛使用的作業(yè)調(diào)度框架,擁有強(qiáng)大的社區(qū)支持和豐富的生態(tài)系統(tǒng)。
- XXL-Job:XXL-Job也有一個活躍的社區(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)線程+休眠實現(xiàn)定時任務(wù),是最簡單實現(xiàn)定時任務(wù)的方式了,但這只是提供一種思路,實習(xí)開發(fā)中幾乎不會使用。
(2)JDK自帶的定時任務(wù)Timer和ScheduledExecutorService,我們需要了解兩者的區(qū)別。
- Timer是單線程的,一旦發(fā)生異常,將終止所有的任務(wù);Timer是絕對時間的,會受到系統(tǒng)時間的影響。
- ScheduledExecutorService是基于線程池,是多線程的,一旦發(fā)生異常,不會終止所有的任務(wù);ScheduledExecutorService是相對時間 ,不會受到系統(tǒng)時間的影響。
- 注意區(qū)固定間隔和固定頻率的區(qū)別。
(3)Spring Task實現(xiàn)的定時任務(wù)是基于線程池,是多線程的,一旦發(fā)生異常,不會終止所有的任務(wù);基于相對時間,不會受到系統(tǒng)時間的影響。
(4)分布式定時任務(wù),一般是直接使用第三方成熟的定時任務(wù)框架,當(dāng)然如果你公司資金充足可以選擇開發(fā)定制化定時任務(wù)框架。選用開源的第三方成熟定時任務(wù)框架,好處在于功能完善、免費,代碼質(zhì)量也是有保障的。
如果你當(dāng)前系統(tǒng)比較小,或者說沒那么在意可靠性,可以選用 JDK自帶的定時任務(wù)或者是SpringTask,否則就選用分布式定時任務(wù)框架,輕量級就可以選用 XXL-Job,大型系統(tǒng)可以選用Quartz。
到此這篇關(guān)于Java中實現(xiàn)定時任務(wù)的兩種方法的文章就介紹到這了,更多相關(guān)Java實現(xiàn)定時任務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot內(nèi)嵌tomcat應(yīng)用原理深入分析
懂得SpringBoot的童鞋應(yīng)該很清楚,不管應(yīng)用程序是屬于何種類型,都是一個Main方法走遍天下,對于web應(yīng)用,只需要引入spring-boot-starter-web中這個依賴,應(yīng)用程序就好像直接給我們來了個tomcat一樣,對于嵌入式Tomcat,其實也非常簡單,就是調(diào)用Tomcat提供的外部類2022-09-09
SpringBoot集成Redis實現(xiàn)驗證碼的簡單案例
本文主要介紹了SpringBoot集成Redis實現(xiàn)驗證碼的簡單案例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08
java實現(xiàn)圖片格式轉(zhuǎn)換(PNG轉(zhuǎn)BMP)
隨著各類圖像格式廣泛應(yīng)用,不同平臺與系統(tǒng)對圖片格式的兼容性需求不斷提升,下面小編就來和大家詳細(xì)介紹一下如何使用java實現(xiàn)PNG圖片格式轉(zhuǎn)為BMP圖片格式2025-06-06
java并發(fā)數(shù)據(jù)包Exchanger線程間的數(shù)據(jù)交換器
這篇文章主要為大家介紹了java并發(fā)數(shù)據(jù)包使用數(shù)據(jù)交換器Exchanger來進(jìn)行線程之間的數(shù)據(jù)交換。有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03
解決Java調(diào)用BAT批處理不彈出cmd窗口的方法分析
本篇文章是對Java調(diào)用BAT批處理不彈出cmd窗口的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
spring @EventListener 事件與監(jiān)聽的示例詳解
本文介紹了自定義Spring事件和監(jiān)聽器的方法,包括如何發(fā)布事件、監(jiān)聽事件以及如何處理異步事件,通過示例代碼和日志,展示了事件的順序執(zhí)行和異步處理機(jī)制,感興趣的朋友一起看看吧2025-03-03

