【JAVA定時器】四種常見定時器的原理和簡單實現
個人學習筆記分享,當前能力有限,請勿貶低,菜鳥互學,大佬繞道
如有勘誤,歡迎指出和討論,本文後期也會進行修正和補充
前言
定時器顧名思義,即定時觸發某個事件,分離開來,即包含三個因素:定時,觸發,某個事件,本文也將以此為基礎介紹五種常見的定時器
本文只做基於SpringBoot的示例,其餘版本的請自行查閱資料,大同小異
1.介紹
1.1.目的
定時器的目的即為了在某個時間點,程式自身主動觸發某個事件,而不需要外力去開啟或者啟動,以節省人力並統一管理
1.2.示例場景
- 管理系統,需要每日12點將前一天的資料進行備份,並生成歷史資料統計
- 宿管系統,每日10點將所有未歸人員統計出來,主動交由管理人員
- 硬體裝置,需要每隔2分鐘檢查裝置是否連線正常,裝置異常需要更新狀態到管理端,必要時通知有關人員
- 圖書館借書管理系統,每天12點需要檢查即將超時和已超時歸還的書籍,並通過簡訊或其他途徑通知有關人員
- 手機下載管理系統,開啟下載後每隔0.5s重新整理一次下載進度,在下載完成或者長時間卡頓時告知使用者
- 訂單管理系統,使用者下達訂單後開需要在半小時內付款,成功付款則生成訂單結果,超時未付款則自動取消訂單
是不是覺得很常見?
1.3.常見實現方案
- @Scheduled註解:基於註解
- Timer().schedule建立任務:基於封裝類
Timer
- 執行緒:使用執行緒直接執行任務即可,可以與thread、執行緒池、ScheduleTask等配合使用
- quartz配置定時器:基於
spring
的quartz
框架
本文僅簡述前3種,比較簡單易懂,quartz會專門分離出來整理
2.@Scheduled註解
2.1.介紹:
使用註解標記需要定時執行的方法,並設定執行時間,便可使其在指定的時間執行指定方法
2.2.步驟:
- 使用註解
@Scheduled
標記目標方法,引數為執行時間 - 使用註解
@EnableScheduling
標記目標方法所在的類,或者直接標記專案啟動類
2.3.註解:
- 註解
@Scheduled
為方法註解,用於標記某個方法在何時定時執行 - 需要配合另一個註解
@EnableScheduling
進行使用,該註解用於標記某個類,開啟定時任務,通常標記在定時器所在的類,或者直接設定在專案啟動類上
2.4.@Scheduled引數:
-
@Scheduled(fixedDelay = 5000)
:方法執行完成後等待5秒再次執行 -
@Scheduled(fixedRate = 5000)
:方法每隔5秒執行一次 -
@Scheduled(initialDelay=1000, fixedRate=5000)
:延遲1秒後執行第一次,之後每隔5秒執行一次 -
fixedDelayString
、fixedRateString
、initialDelayString
:與上訴三種作用一直,但引數為字串型別,因而可以使用佔位符,形如@Scheduled(fixedDelayString = "${time.fixedDelay}")
-
@Scheduled(cron = "0 0,30 0,8 ? * ? ")
:方法在每天的8點30分0秒執行,引數為字串型別,那麼同理也可使用佔位符,cron表示式請另行查閱資料,推薦看這篇文章:https://www.jianshu.com/p/1defb0f22ed1
2.5.示例
示例1:每隔3秒執行一次
@Component
@EnableScheduling
public class ScheduleTest {
private int count = 0;
/**
* 每3秒鐘執行一次
*/
@Scheduled(cron = "*/3 * * * * ?")
public void test1() {
System.out.println(count + ":" + (new Date()).toString());
count++;
}
}
示例2:第一次等待10秒,之後每3秒一次
@Component
@EnableScheduling
public class ScheduleTest {
private int count = 0;
/**
* 第一次等待10秒,之後每3秒鐘執行一次
*/
@Scheduled(initialDelay = 10000, fixedRate = 3000)
public void test1() {
System.out.println(count + ":" + (new Date()).toString());
count++;
}
}
2.6.小結
- 優勢:簡單便捷,僅兩行註解便完成了定時效果
- 劣勢:所有引數和執行的方法必須提前寫入程式碼裡,可擴充套件性極低
3.Timer().schedule建立任務
3.1.樣例
使用非常簡單,這裡先給出樣例,在對照進行介紹
程式碼如下
package com.yezi_tool.demo_basic.test;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
@Component
public class TimerTest {
private Integer count = 0;
public TimerTest() {
testTimer();
}
public void testTimer() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
try {
//do Something
System.out.println(new Date().toString() + ": " + count);
count++;
} catch (Exception e) {
e.printStackTrace();
}
}
}, 0, 1000);
}
}
執行結果
可以看到每隔1s列印一次count並自增1
3.2.介紹
核心包括Timer和TimerTask,均為jkd自帶的工具類,程式碼量分別為721行和162行(包括註釋),都不多,有興趣的可以直接看看原始碼
3.2.1.TimerTask
TimerTask實際上就是一個Runnable而已,繼承Runnable並添加了幾個自定義的引數和方法,沒啥好介紹的,有興趣可以看原始碼
3.2.2.Timer
Timer字面意思即定時器,為jkd自帶的工具類,提供定時執行任務的相關功能
實際上包括三個類:
-
Timer:即定時器主類,負責管理所有的定時任務,每個Timer擁有一個私有的TaskQueue和TimerThread,
-
TaskQueue:即任務佇列,Timer生產任務,然後推到TaskQueue裡存放,等待處理,被處理掉的任務即被移除掉
TaskQueue
實質上只有一個長度為128的陣列用於儲存TimerTask
、一個int型變數size表示佇列長度、以及對這兩個資料的增刪改查 -
TimerThread:即定時器執行緒,執行緒會共享TaskQueue裡面的資料,TimerThread會對TaskQueue裡的任務進行消耗
TimerThread
實際上就是一個Thread
執行緒,會不停的監聽TaskQueue
,如果佇列裡面有任務,那麼就執行第一個,並將其刪除(先刪除再執行)
流程分析
Timer
生產任務(實際上是從外部接收到任務),並將任務推到TaskQueue
裡面存放,並喚醒TaskQueue
執行緒(queue.notify()
)TimerThread
監聽TaskQueue
,若裡面有任務則將其執行並移除隊裡,若沒有任務則讓佇列等待(queue.wait()
)
這麼一看,這不就是典型的生產者/消費者模式,timer
負責生產(實際上是接受),而TimerThread
負責消費,TaskQueue
作為中轉倉庫
構造方法
構造的時候會設定定時器執行緒的名字並將其啟動
完整格式如下,其中兩個引數均可預設
public Timer(String name, boolean isDaemon)
- name:即執行緒名,用於區分不同的執行緒,預設的時候預設使用
"Timer-" + serialNumber()
生成唯一執行緒名- isDaemon:是否是守護執行緒,預設的時候預設為否,有啥區別請自行了解,有機會的話我也會整理筆記
核心方法
核心方法有新增任務、取消任務和淨化三種
新增任務有6中公用方法(實際最後使用同一種私有方法)
- schedule(TimerTask task, long delay):指定任務task,在delay毫秒延遲後執行
- schedule(TimerTask task, Date time):指定任務task,在time時間點執行一次
- schedule(TimerTask task, long delay, long period):指定任務task,延遲delay毫秒後執行第一次,並在之後每隔period毫秒執行一次
- schedule(TimerTask task, Date firstTime, long period):指定任務task,在firstTime的時候執行第一次,之後每隔period毫秒執行一次
- scheduleAtFixedRate(TimerTask task, long delay, long period):作用與schedule一致
- scheduleAtFixedRate(TimerTask task, Date firstTime, long period):作用與schedule一致
實際上最後都會使用
sched(TimerTask task, long time, long period)
,即指定任務task,在time執行第一次,之後每隔period毫秒執行一次
schedule
使用系統時間計算下一次,即System.currentTimeMillis()+period
而
scheduleAtFixedRate
使用本次預計時間計算下一次,即time + period
對於耗時任務,兩者區別較大,請按需求選擇,瞬時任務無區別
取消任務方法:cancel(),會將任務佇列清空,並堵塞執行緒,且不再能夠接受任務(接受時報錯),並不會銷燬本身的例項和其內部的執行緒
淨化方法:purge(),淨化會將佇列裡所有被取消的任務移除,對剩餘任務進行堆排序,並返回移除任務的數量
補充
-
如何保證第一個任務是執行時間最早的
任務佇列會在每一次新增任務和刪除任務時,進行堆排序矯正,淨化也會對剩餘任務重新堆排序
-
cancel的時候執行緒如何處理
定時器執行緒進行堵塞處理,並沒有銷燬,在執行當前任務後就不會執行下一次了,但是執行緒並沒有銷燬
所以儘量不要建立太多timer物件,會增加伺服器負擔
3.3.使用步驟
-
初始化Timer
Timer timer=new Timer();
-
初始化task
private class MyTask extends TimerTask { @Override public void run() { try { //do Something System.out.println(new Date().toString() + ": " + count); count++; } catch (Exception e) { e.printStackTrace(); } } } }
MyTask myTask=new MyTask();
-
新增任務
timer.schedule(myTask, 5000, 3000);
完整程式碼:
package com.yezi_tool.demo_basic.test; import org.springframework.stereotype.Component; import java.util.Date; import java.util.Timer; import java.util.TimerTask; @Component public class TimerTest { private Integer count = 0; public TimerTest() { testTimer2(); } public void testTimer2() { Timer timer = new Timer(); MyTask myTask = new MyTask(); timer.schedule(myTask, 0, 1000); } private class MyTask extends TimerTask { @Override public void run() { try { //do Something System.out.println(new Date().toString() + ": " + count); count++; } catch (Exception e) { e.printStackTrace(); } } } }
當然可以縮寫為樣例裡面的寫法,更加簡潔,請按照自己需求修改
4.執行緒
執行緒應該是最常見的實現方案,建立一個執行緒執行任務即可,舉例幾個不同的寫法,程式碼如下
4.1.使用thread + runnable
package com.yezi_tool.demo_basic.test;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class ThreadTest {
private Integer count = 0;
public ThreadTest() {
test1();
}
public void test1() {
new Thread(() -> {
while (count < 10) {
System.out.println(new Date().toString() + ": " + count);
count++;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
4.2.使用執行緒池 + runnable
package com.yezi_tool.demo_basic.test;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Component
public class ThreadTest {
private static final ExecutorService threadPool = Executors.newFixedThreadPool(5);// 執行緒池
private Integer count = 0;
public ThreadTest() {
test2();
}
public void test2() {
threadPool.execute(() -> {
while (count < 10) {
System.out.println(new Date().toString() + ": " + count);
count++;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
4.3.使用ScheduledTask + runnable
ScheduledTask 有11種新增任務的方法,詳情直接檢視檔案TaskScheduler.java,這裡給出常用的幾個示例
-
設定觸發頻率為3000毫秒
package com.yezi_tool.demo_basic.test; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Component; import java.util.Date; @Component public class ThreadTest { private Integer count = 0; private final TaskScheduler taskScheduler; public ThreadTest(TaskScheduler taskScheduler) { this.taskScheduler = taskScheduler; test3(); } public void test3() { taskScheduler.scheduleAtFixedRate(() -> { System.out.println(new Date().toString() + ": " + count); count++; }, 3000); } }
-
設定觸發時間為每天凌晨1點
package com.yezi_tool.demo_basic.test; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Component; import java.util.Date; @Component public class ThreadTest { private Integer count = 0; private final TaskScheduler taskScheduler; public ThreadTest(TaskScheduler taskScheduler) { this.taskScheduler = taskScheduler; test4(); } public void test4() { taskScheduler.schedule(() -> { System.out.println(new Date().toString() + ": " + count); count++; }, new CronTrigger("0 0 1 * * ?")); } }
5.quartz
專門整理了一篇quartz的筆記,有興趣的可以看我上一篇部落格
寫的並不完善,後續應該會進行修正
6.總結
- @schedule使用方便快捷,但功能有限,擴充套件性極低,適用於不需要統一管理的簡單場景
- Timer可以統一管理定時任務,但自身作為一個工具類,功能較少,但是也適用於很多場景了
- 執行緒的使用同樣比較方便,靈活度特別高,支援各種型別的觸發時間,但畢竟沒有專用的框架,功能並不算特別齊全,適用於對自由度要求較高的場景
- quartz作為專門的定時器專案,功能齊全且強大,目前大部分專案仍只使用了其小部分功能,適用於要求較高的場景
BB兩句
其實除了@schedule,其餘的都可以自定義管理器,來統一管理,並動態修改,具體咋做此處先不做贅述
quartz已經整理除了靜態定時器和動態定時器,有興趣的可以瞅瞅
作者:Echo_Ye
WX:Echo_YeZ
EMAIL :[email protected]
個人站點:在搭了在搭了。。。(右鍵 - 新建資料夾)