個人學習筆記分享,當前能力有限,請勿貶低,菜鳥互學,大佬繞道
如有勘誤,歡迎指出和討論,本文后期也會進行修正和補充
前言
定時器顧名思義,即定時觸發某個事件,分離開來,即包含三個因素:定時,觸發,某個事件,本文也將以此為基礎介紹五種常見的定時器
本文只做基於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作為專門的定時器項目,功能齊全且強大,目前大部分項目仍只使用了其小部分功能,適用於要求較高的場景
7.demo地址
https://gitee.com/echo_ye/demo_basic/tree/scheduleDemo
不同定時器啟用方法在README.MD
中查看,一共6種方法,如有紕漏請聯系我
僅實現了部分功能作為樣例,請按照需求自己擴展哦,有疑問或者建議歡迎聯系我~
BB兩句
其實除了@schedule,其余的都可以自定義管理器,來統一管理,並動態修改,具體咋做此處先不做贅述
quartz已經整理除了靜態定時器和動態定時器,有興趣的可以瞅瞅
作者:Echo_Ye
WX:Echo_YeZ
EMAIL :echo_yezi@qq.com
個人站點:在搭了在搭了。。。(右鍵 - 新建文件夾)