Java定時任務技術分析

常見的業務場景:

某部落格平臺,支援定時傳送文章。

某學習平臺,定時傳送學習任務通知使用者

定時進行資料抓取等等

在專案中要求我們在某個時刻去做某件事情,下面我們就來看看有哪些方法可以實現定時任務。

JDK內建類

Timer

java。util。Timer是 JDK 1。3 開始就已經支援的一種定時任務的實現方式。

Timer 內部使用一個叫做 TaskQueue 的類存放定時任務,它是一個基於最小堆實現的優先順序佇列。TaskQueue 會按照任務距離下一次執行時間的大小將任務排序,保證在堆頂的任務最先執行。這樣在需要執行任務時,每次只需要取出堆頂的任務執行即可!由於某個任務的執行時間可能較長,則後面的任務執行的時間會被延遲,所以執行的時間和你預期的時間可能不一致。延遲的任務具體開始的時間,就是依據前面任務的結束時間。

核心方法:

//啟動任務之後,延遲多久時間執行void schedule(TimerTask task, long delay);//在指定的時間執行任務void schedule(TimerTask task, Date time);//啟動任務後,延遲多久時間執行,執行之後指定間隔多久重複執行任務void schedule(TimerTask task, long delay, long period);//指定時間啟動任務,執行後間隔指定時間重複執行任務void schedule(TimerTask task, Date firstTime, long period);

程式碼案例:

public class TimerUse { public static void main(String[] args) { System。out。println(“當前時間: ” + new Date() + “n” + “執行緒名稱: ” + Thread。currentThread()。getName()); testTimer1();// testTimer2();// testTimer3();// testTimer4(); } // 方法一:設定指定任務task在指定時間time執行 schedule(TimerTask task, long delay) public static void testTimer1() { Timer timer = new Timer(“Timer”); timer。schedule(new TimerTask() { public void run() { System。out。println(“當前時間: ” + new Date() + “n” + “執行緒名稱: ” + Thread。currentThread()。getName()); } }, 3500); // 設定指定的時間time為3500毫秒 } /** * 方法二:設定指定任務task在指定延遲delay後間隔指定時間peroid執行 schedule(TimerTask task, long delay, long period) */ public static void testTimer2() { Timer timer = new Timer(“Timer”); timer。schedule(new TimerTask() { public void run() { System。out。println(“當前時間: ” + new Date() + “n” + “執行緒名稱: ” + Thread。currentThread()。getName()); } }, 2000, 3500); } /** * 方法三:在指定的時間執行任務 schedule(TimerTask task, Date time) */ public static void testTimer3() { Date date = new Date(); Calendar calendar = Calendar。getInstance(); calendar。setTime(date); calendar。add(Calendar。MINUTE, 1); // 往後推一分鐘 Date time = calendar。getTime(); //獲取當前系統時間 Timer timer = new Timer(“Timer”); timer。schedule(new TimerTask() { public void run() { System。out。println(“當前時間: ” + new Date() + “n” + “執行緒名稱: ” + Thread。currentThread()。getName()); } }, time); } /** * 方法四:安排指定的任務task在指定的時間firstTime開始進行重複的固定速率period執行. schedule(TimerTask task, Date firstTime, * long period) */ public static void testTimer4() { Calendar calendar = Calendar。getInstance(); calendar。set(Calendar。HOUR_OF_DAY, 12); // 控制小時 calendar。set(Calendar。MINUTE, 0); // 控制分鐘 calendar。set(Calendar。SECOND, 0); // 控制秒 Date time = calendar。getTime(); //獲取當前系統時間 Timer timer = new Timer(“Timer”); timer。schedule(new TimerTask() { public void run() { System。out。println(“當前時間: ” + new Date() + “n” + “執行緒名稱: ” + Thread。currentThread()。getName()); } }, time, 1000 * 60 * 60 * 24);// 這裡設定將延時每天固定執行 }}

注意事項

1、建立一個 Timer 物件相當於新啟動了一個執行緒,但是這個新啟動的執行緒,並不是守護執行緒。它一直在後臺執行,透過如下程式碼將新啟動的 Timer 執行緒設定為守護執行緒。

Timer timer = new Timer(true);

變為守護執行緒,則意味著主執行緒執行結束,則程式就結束了,定時任務也就不會執行。

2、當計劃時間早於當前時間,則任務立即被執行。

ScheduledExecutorService

ScheduledExecutorService 是一個介面,有多個實現類,比較常用的是 ScheduledThreadPoolExecutor 。

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {}

Java定時任務技術分析

ScheduledThreadPoolExecutor 的狀態管理、入隊操作、拒絕操作等都是繼承於 ThreadPoolExecutor;ScheduledThreadPoolExecutor 主要是提供了週期任務和延遲任務相關的操作;

schedule(Runnable command, long delay, TimeUnit unit) // 無返回值的延遲任務schedule(Callable callable, long delay, TimeUnit unit) // 有返回值的延遲任務scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) // 固定頻率週期任務scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) // 固定延遲週期任務

程式碼示例:

TimerTask repeatedTask = new TimerTask() { @SneakyThrows public void run() { System。out。println(“當前時間: ” + new Date() + “n” + “執行緒名稱: ” + Thread。currentThread()。getName()); }};System。out。println(“當前時間: ” + new Date() + “n” + “執行緒名稱: ” + Thread。currentThread()。getName());ScheduledExecutorService executor = Executors。newScheduledThreadPool(1);long delay = 1L;long period = 2L;// 延遲1s,週期2sexecutor。scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit。SECONDS);// 定時任務重複執行3個週期Thread。sleep((delay + period * 3) * 1000);executor。shutdown();

執行結果為:

當前時間: Wed Nov 23 09:52:35 CST 2022n執行緒名稱: main當前時間: Wed Nov 23 09:52:36 CST 2022n執行緒名稱: pool-1-thread-1當前時間: Wed Nov 23 09:52:38 CST 2022n執行緒名稱: pool-1-thread-1當前時間: Wed Nov 23 09:52:40 CST 2022n執行緒名稱: pool-1-thread-1當前時間: Wed Nov 23 09:52:42 CST 2022n執行緒名稱: pool-1-thread-1

注意事項

1、scheduleAtFixedRate 和 scheduleWithFixedDelay 是我們最常用的兩個方法,兩者略有區別,前者為固定頻率週期任務,如果任務執行時間超出週期時,下一次任務會立刻執行;後者為固定延遲週期任務,

無論執行時間是多少,其結果都是在執行完畢後,停頓固定的時間,然後執行下一次任務

2、ScheduledThreadPoolExecutor 執行緒最多為核心執行緒,最大執行緒數不起作用,因為 DelayedWorkQueue 是無界佇列。

更多內容推薦閱讀:併發系列(7)之 ScheduledThreadPoolExecutor 詳解

小結

在 JDK 中,內建了兩個類,可以實現定時任務的功能:

java。util。Timer :可以透過建立 java。util。TimerTask 排程任務,在同一個執行緒中

序列

執行,相互影響。也就是說,對於同一個 Timer 裡的多個 TimerTask 任務,如果一個 TimerTask 任務在執行中,其它 TimerTask 即使到達執行的時間,也只能排隊等待。因為 Timer 是序列的,同時存在 坑坑 ,所以後來 JDK 又推出了 ScheduledExecutorService ,Timer 也基本不再使用。

java。util。concurrent。ScheduledExecutorService :在 JDK 1。5 新增,基於執行緒池設計的定時任務類,每個排程任務都會被分配到執行緒池中

併發

執行,互不影響。這樣,ScheduledExecutorService 就解決了 Timer 序列的問題。

在日常開發中,我們很少直接使用 Timer 或 ScheduledExecutorService 來實現定時任務的需求。主要有幾點原因:

它們僅支援按照指定頻率,不直接支援指定時間的定時排程,需要我們結合 Calendar 自行計算,才能實現複雜時間的排程。例如說,每天、每週五、2019-11-11 等等,不支援 Cron 表示式。

它們是程序級別,而我們為了實現定時任務的高可用,需要部署多個程序。此時需要等多考慮,多個程序下,同一個任務在相同時刻,不能重複執行。

專案可能存在定時任務較多,需要統一的管理,此時不得不進行二次封裝。

所以,一般情況下,我們會選擇專業的

排程任務中介軟體

中介軟體

Spring Task

由於 SpringTask 已經存在於 Spring 框架中,所以無需新增依賴。

下面我們弄個小 Demo 測試一下,新建一個專案,引入依賴。

org。springframework。boot spring-boot-starter-parent 2。6。3 org。springframework。boot spring-boot-starter-web org。projectlombok lombok 1。18。20

新增 SpringTask 的配置類

@Configuration@EnableSchedulingpublic class SpringTaskConfig {}

在 application。yml 新增關於 Spring Task 的配置,如下:

spring: task: # Spring Task 排程任務的配置,對應 TaskSchedulingProperties 配置類 scheduling: thread-name-prefix: job- # 執行緒池的執行緒名的字首。預設為 scheduling- ,建議根據自己應用來設定 pool: size: 10 # 執行緒池大小。預設為 1 ,根據自己應用來設定 shutdown: await-termination: true # 應用關閉時,是否等待定時任務執行完成。預設為 false ,建議設定為 true await-termination-period: 60 # 等待任務完成的最大時長,單位為秒。預設為 0 ,根據自己應用來設定

spring。task。scheduling。shutdown 配置項,是為了實現 Spring Task 定時任務的優雅關閉。

定時任務測試類

@Service@Slf4jpublic class ScheduledTaskService { private final AtomicInteger counts = new AtomicInteger(); // @Scheduled(cron = “0 0/10 * ? * ?”)//每10分鐘執行一次 @Scheduled(fixedRate = 3000) // 每 3秒執行一次 public void pushMessage() { log。info(“[execute]定時第({})給使用者傳送通知”, counts。incrementAndGet()); }}

最後建立一個啟動類,啟動專案,控制檯輸出如下:

Java定時任務技術分析

Spring Task 支援

Cron 表示式

。Cron 表示式主要用於定時作業(定時任務)系統定義執行時間或執行頻率的表示式,非常厲害,你可以透過 Cron 表示式進行設定定時任務每天或者每個月什麼時候執行等等操作。

Cron 格式中每個時間元素的說明

Java定時任務技術分析

平時可以找一個 Cron 表示式生成器線上網站按需生成想要的表示式。

SpringTask 功能小結:

1、SpringTask 內置於 Spring 框架,相比於Quartz更加簡單方便,不需要引入其他依賴。

2、Spring Task 底層是基於 JDK 的 ScheduledThreadPoolExecutor 執行緒池來實現的。

3、支援 Cron 表示式

4、只支援單機,功能單一

Quartz

Github:github。com/quartz-sche…

Quartz 作為一個優秀的開源排程框架,Quartz 具有以下特點:

強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種常規及特殊需求;

靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種儲存方式;

分散式和叢集能力,Terracotta 收購後在原來功能基礎上作了進一步提升。

另外,作為 Spring 預設的排程框架,Quartz 很容易與 Spring 整合實現靈活可配置的排程功能。

在 Quartz 體系結構中,有三個元件非常重要:

Scheduler :排程器。Scheduler啟動Trigger去執行Job。

Trigger :觸發器。用來定義 Job(任務)觸發條件、觸發時間,觸發間隔,終止時間等。四大型別:SimpleTrigger(簡單的觸發器)、CornTrigger(Cron表示式觸發器)、DateIntervalTrigger(日期觸發器)、CalendarIntervalTrigger(日曆觸發器)。

Job :任務。具體要執行的業務邏輯,比如:傳送簡訊、傳送郵件、訪問資料庫、同步資料等。

Quartz 應用分為單機模式和叢集模式。實際應用中,我們都會選擇叢集模式,關於 Quartz 的使用,後續會單獨出一篇文章進行介紹。

Quartz 框架出現的比較早,後續不少定時框架,或多或少都基於 Quartz 研發的,比如噹噹網的elastic-job就是基於quartz二次開發之後的分散式排程解決方案。

並且,Quartz 並沒有內建 UI 管理控制檯,不過你可以使用 quartzui 這個開源專案來解決這個問題。

Quartz 雖然也支援分散式任務。但是,它是在資料庫層面,透過資料庫的鎖機制做的,有非常多的弊端比如系統侵入性嚴重、節點負載不均衡。

Quartz 優缺點:

可以與 Spring 整合,並且支援動態新增任務和叢集。

分散式支援不友好,沒有內建 UI 管理控制檯、使用麻煩(相較於其他框架)。

XXL-JOB

官方說明:XXL-JOB 是一個輕量級分散式任務排程平臺,其核心設計目標是開發迅速、學習簡單、輕量級、易擴充套件。現已開放原始碼並接入多家公司線上產品線,開箱即用。

通俗來講:XXL-JOB 是一個任務排程框架,透過引入 XXL-JOB 相關的依賴,按照相關格式撰寫程式碼後,可在其視覺化介面進行任務的

啟動

執行

中止

以及包含了

日誌記錄與查詢

任務狀態監控

特性:

Java定時任務技術分析

Xxl-job 解決了很多 Quartz 的不足。

Java定時任務技術分析

XXL-JOB 的架構設計如下圖所示:

Java定時任務技術分析

從上圖可以看出,XXL-JOB 由

排程中心

執行器

兩大部分組成。排程中心主要負責任務管理、執行器管理以及日誌管理。執行器主要是接收排程訊號並處理。另外,排程中心進行任務排程時,是透過自研 RPC 來實現的。

關於 xxl-job 的使用,下篇文章會詳細介紹的。

xxl-job 的優點相對於 Quartz 非常明顯,使用更加簡單,而且內建了 UI 管理控制檯。

Elastic-Job…

ElasticJob 是面向網際網路生態和海量任務的分散式排程解決方案,基於Quartz和ZooKeeper 開發,由兩個相互獨立的子專案 ElasticJob-Lite 和 ElasticJob-Cloud 組成。 它透過彈性排程、資源管控、以及作業治理的功能,打造一個適用於網際網路場景的分散式排程解決方案,並透過開放的架構設計,提供多元化的作業生態。 它的各個產品使用統一的作業 API,開發者僅需一次開發,即可隨意部署。

ElasticJob-Lite 的架構設計如下圖所示:

Java定時任務技術分析

從上圖可以看出,Elastic-Job 沒有排程中心這一概念,而是使用 ZooKeeper 作為註冊中心,註冊中心負責協調分配任務到不同的節點上。

Elastic-Job 中的定時排程都是由執行器自行觸發,這種設計也被稱為去中心化設計(排程和處理都是執行器單獨完成)。

@Component@ElasticJobConf(name = “dayJob”, cron = “0/10 * * * * ?”, shardingTotalCount = 2, shardingItemParameters = “0=AAAA,1=BBBB”, description = “簡單任務”, failover = true)public class TestJob implements SimpleJob { @Override public void execute(ShardingContext shardingContext) { log。info(“TestJob任務名:【{}】, 片數:【{}】, param=【{}】”, shardingContext。getJobName(), shardingContext。getShardingTotalCount(), shardingContext。getShardingParameter()); }}

Elastic-Job 支援的功能:

Java定時任務技術分析

關於 Elastic-Job 的使用,未來會抽時間出一篇文章的。

Elastic-Job 相較於 XXL-JOB 缺點也比較明顯,就是需要引入額外的中介軟體,比如 Zookeeper,增加了操作難度。

作者:hresh

連結:https://juejin。cn/post/7171604341437136909