深入了解Java定時(shí)器中的Timer的原理
Java在1.3版本引入了Timer工具類,它是一個(gè)古老的定時(shí)器,搭配TimerTask和TaskQueue一起使用。從Java5開始在并發(fā)包中引入了另一個(gè)定時(shí)器
ScheduledThreadPoolExecutor,它對Timer做了很多改進(jìn)并提供了更多的工具,可以認(rèn)為是對Timer的取代。
那為什么還要介紹Timer工具類呢?通過了解Timer的功能和它背后的原理,有助于我們更好的對比了解
ScheduledThreadPoolExecutor,同時(shí)ScheduledThreadPoolExecutor的一些改進(jìn)思想在我們平時(shí)的編碼工作中也可以借鑒。
主要成員變量
Timer中用到的主要是兩個(gè)成員變量:
- TaskQueue:一個(gè)按照時(shí)間優(yōu)先排序的隊(duì)列,這里的時(shí)間是每個(gè)定時(shí)任務(wù)下一次執(zhí)行的毫秒數(shù)(相對于1970年1月1日而言)
- TimerThread:對TaskQueue里面的定時(shí)任務(wù)進(jìn)行編排和觸發(fā)執(zhí)行,它是一個(gè)內(nèi)部無限循環(huán)的線程。
//根據(jù)時(shí)間進(jìn)行優(yōu)先排序的隊(duì)列 private final TaskQueue queue = new TaskQueue(); //消費(fèi)線程,對queue中的定時(shí)任務(wù)進(jìn)行編排和執(zhí)行 private final TimerThread thread = new TimerThread(queue); //構(gòu)造函數(shù) public Timer(String name) { thread.setName(name); thread.start(); }
定時(shí)功能
Timer提供了三種定時(shí)模式:
- 一次性任務(wù)
- 按照固定的延遲執(zhí)行(fixed delay)
- 按照固定的周期執(zhí)行(fixed rate)
第一種比較好理解,即任務(wù)只執(zhí)行一次;針對第一種,Timer提供了以下兩個(gè)方法:
//在當(dāng)前時(shí)間往后delay個(gè)毫秒開始執(zhí)行 public void schedule(TimerTask task, long delay) {...} //在指定的time時(shí)間點(diǎn)執(zhí)行 public void schedule(TimerTask task, Date time) {...}
第二種Fixed Delay模式也提供了以下兩個(gè)方法
//從當(dāng)前時(shí)間開始delay個(gè)毫秒數(shù)開始定期執(zhí)行,周期是period個(gè)毫秒數(shù) public void schedule(TimerTask task, long delay, long period) {...} ////從指定的firstTime開始定期執(zhí)行,往后每次執(zhí)行的周期是period個(gè)毫秒數(shù) public void schedule(TimerTask task, Date firstTime, long period){...}
它的工作方式是:
第一次執(zhí)行的時(shí)間將按照指定的時(shí)間點(diǎn)執(zhí)行(如果此時(shí)TimerThread不在執(zhí)行其他任務(wù)),如有其他任務(wù)在執(zhí)行,那就需要等到其他任務(wù)執(zhí)行完成才能執(zhí)行。
從第二次開始,每次任務(wù)的執(zhí)行時(shí)間是上一次任務(wù)開始執(zhí)行的時(shí)間加上指定的period毫秒數(shù)。
如何理解呢,我們還是看代碼
public static void main(String[] args) { TimerTask task1 = new DemoTimerTask("Task1"); TimerTask task2 = new DemoTimerTask("Task2"); Timer timer = new Timer(); timer.schedule(task1, 1000, 5000); timer.schedule(task2, 1000, 5000); } static class DemoTimerTask extends TimerTask { private String taskName; private DateFormat df = new SimpleDateFormat("HH:mm:ss---"); public DemoTimerTask(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println(df.format(new Date()) + taskName + " is working."); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(df.format(new Date()) + taskName + " finished work."); } }
task1和task2是幾乎同時(shí)執(zhí)行的兩個(gè)任務(wù),而且執(zhí)行時(shí)長都是2秒鐘,如果此時(shí)我們把第六行注掉不執(zhí)行,我們將得到如下結(jié)果(和第三種Fixed Rate模式結(jié)果相同):
13:42:58---Task1 is working.
13:43:00---Task1 finished work.
13:43:03---Task1 is working.
13:43:05---Task1 finished work.
13:43:08---Task1 is working.
13:43:10---Task1 finished work.
如果打開第六行,我們再看下兩個(gè)任務(wù)的執(zhí)行情況。我們是期望兩個(gè)任務(wù)能夠同時(shí)執(zhí)行,但是Task2是在Task1執(zhí)行完成后才開始執(zhí)行(原因是TimerThread是單線程的,每個(gè)定時(shí)任務(wù)的執(zhí)行也在該線程內(nèi)完成,當(dāng)多個(gè)任務(wù)同時(shí)需要執(zhí)行時(shí),只能是阻塞了),從而導(dǎo)致Task2第二次執(zhí)行的時(shí)間是它上一次執(zhí)行的時(shí)間(13:43:57)加上5秒鐘(13:44:02)。
13:43:55---Task1 is working.
13:43:57---Task1 finished work.
13:43:57---Task2 is working.
13:43:59---Task2 finished work.
13:44:00---Task1 is working.
13:44:02---Task1 finished work.
13:44:02---Task2 is working.
13:44:04---Task2 finished work.
那如果此時(shí)還有個(gè)Task3也是同樣的時(shí)間點(diǎn)和間隔執(zhí)行會怎么樣呢?
結(jié)論是:也將依次排隊(duì),執(zhí)行的時(shí)間依賴兩個(gè)因素:
1.上次執(zhí)行的時(shí)間
2.期望執(zhí)行的時(shí)間點(diǎn)上有沒有其他任務(wù)在執(zhí)行,有則只能排隊(duì)了
我們接下來看下第三種Fixed Rate模式,我們將上面的代碼稍作修改:
public static void main(String[] args) { TimerTask task1 = new DemoTimerTask("Task1"); TimerTask task2 = new DemoTimerTask("Task2"); Timer timer = new Timer(); timer.scheduleAtFixedRate(task1, 1000, 5000); timer.scheduleAtFixedRate(task2, 1000, 5000); } static class DemoTimerTask extends TimerTask { private String taskName; private DateFormat df = new SimpleDateFormat("HH:mm:ss---"); public DemoTimerTask(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println(df.format(new Date()) + taskName + " is working."); try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(df.format(new Date()) + taskName + " finished work."); } }
Task1和Task2還是在相同的時(shí)間點(diǎn),按照相同的周期定時(shí)執(zhí)行任務(wù),我們期望Task1能夠每5秒定時(shí)執(zhí)行任務(wù),期望的時(shí)間點(diǎn)是:14:21:47-14:21:52-14:21:57-14:22:02-14:22:07,實(shí)際上它能夠交替著定期執(zhí)行,原因是Task2也會定期執(zhí)行,并且對TaskQueue的鎖他們是交替著拿的(這個(gè)在下面分析TimerThread源碼的時(shí)候會講到)
14:21:47---Task1 is working.
14:21:49---Task1 finished work.
14:21:49---Task2 is working.
14:21:51---Task2 finished work.
14:21:52---Task2 is working.
14:21:54---Task2 finished work.
14:21:54---Task1 is working.
14:21:56---Task1 finished work.
14:21:57---Task1 is working.
14:21:59---Task1 finished work.
14:21:59---Task2 is working.
14:22:01---Task2 finished work.
TimerThread
上面我們主要講了Timer的一些主要源碼及定時(shí)模式,下面我們來分析下支撐Timer的定時(shí)任務(wù)線程TimerThread。
TimerThread大概流程圖如下:
TimerThread流程
源碼解釋如下:
private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // 如果queue里面沒有要執(zhí)行的任務(wù),則掛起TimerThread線程 while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); // 如果TimerThread被激活,queue里面還是沒有任務(wù),則介紹該線程的無限循環(huán),不再接受新任務(wù) if (queue.isEmpty()) break; long currentTime, executionTime; // 獲取queue隊(duì)列里面下一個(gè)要執(zhí)行的任務(wù)(根據(jù)時(shí)間排序,也就是接下來最近要執(zhí)行的任務(wù)) task = queue.getMin(); synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; // taskFired表示是否需要立刻執(zhí)行線程,當(dāng)task的下次執(zhí)行時(shí)間到達(dá)當(dāng)前時(shí)間點(diǎn)時(shí)為true if (taskFired = (executionTime<=currentTime)) { //task.period==0表示這個(gè)任務(wù)只需要執(zhí)行一次,這里就從queue里面刪掉了 if (task.period == 0) { queue.removeMin(); task.state = TimerTask.EXECUTED; } else { // Repeating task, reschedule //針對task.period不等于0的任務(wù),則計(jì)算它的下次執(zhí)行時(shí)間點(diǎn) //task.period<0表示是fixed delay模式的任務(wù) //task.period>0表示是fixed rate模式的任務(wù) queue.rescheduleMin( task.period<0 ? currentTime - task.period : executionTime + task.period); } } } // 如果任務(wù)的下次執(zhí)行時(shí)間還沒有到達(dá),則掛起TimerThread線程executionTime - currentTime毫秒數(shù),到達(dá)執(zhí)行時(shí)間點(diǎn)再自動激活 if (!taskFired) queue.wait(executionTime - currentTime); } // 如果任務(wù)的下次執(zhí)行時(shí)間到了,則執(zhí)行任務(wù) // 注意:這里任務(wù)執(zhí)行沒有另起線程,還是在TimerThread線程執(zhí)行的,所以當(dāng)有任務(wù)在同時(shí)執(zhí)行時(shí)會出現(xiàn)阻塞 if (taskFired) // 這里沒有try catch異常,當(dāng)TimerTask拋出異常會導(dǎo)致整個(gè)TimerThread跳出循環(huán),從而導(dǎo)致Timer失效 task.run(); } catch(InterruptedException e) { } } }
結(jié)論
通過上面的分析,我們可以得出以下結(jié)論:
- Timer支持三種模式的定時(shí)任務(wù)(一次性任務(wù),F(xiàn)ixed Delay模式,F(xiàn)ixed Rate模式)
- Timer中的TimerThread是單線程模式,因此導(dǎo)致所有定時(shí)任務(wù)不能同時(shí)執(zhí)行,可能會出現(xiàn)延遲
- TimerThread中并沒有處理好任務(wù)的異常,因此每個(gè)TimerTask的實(shí)現(xiàn)必須自己try catch防止異常拋出,導(dǎo)致Timer整體失效
Demo代碼位置
到此這篇關(guān)于深入了解Java定時(shí)器中的Timer的原理的文章就介紹到這了,更多相關(guān)Java Timer原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用JDBC工具類實(shí)現(xiàn)簡單的登錄管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了使用JDBC工具類實(shí)現(xiàn)簡單的登錄管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02springboot+vue制作后臺管理系統(tǒng)項(xiàng)目
本文詳細(xì)介紹了后臺管理使用springboot+vue制作,以分步驟、圖文的形式詳細(xì)講解,大家有需要的可以參考參考2021-08-08Spring?Boot?2.6.x整合Swagger啟動失敗報(bào)錯(cuò)問題的完美解決辦法
這篇文章主要給大家介紹了關(guān)于Spring?Boot?2.6.x整合Swagger啟動失敗報(bào)錯(cuò)問題的完美解決辦法,文中通過實(shí)例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-03-03java使用HttpSession實(shí)現(xiàn)QQ訪問記錄
這篇文章主要介紹了java使用HttpSession實(shí)現(xiàn)QQ的訪問記錄的相關(guān)資料,需要的朋友可以參考下2016-03-03Java創(chuàng)建對象(顯式創(chuàng)建和隱含創(chuàng)建)
本文詳細(xì)介紹對象的創(chuàng)建,在 Java 語言中創(chuàng)建對象分顯式創(chuàng)建與隱含創(chuàng)建兩種情況,顯式創(chuàng)建和隱含創(chuàng)建,,需要的朋友可以參考下面文章的具體內(nèi)容2021-09-09JAVA中excel導(dǎo)出一對多合并具體實(shí)現(xiàn)
項(xiàng)目中經(jīng)常會使用到導(dǎo)出功能,有導(dǎo)出Word,有導(dǎo)出Excel的,下面這篇文章主要給大家介紹了關(guān)于JAVA中excel導(dǎo)出一對多合并具體實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2023-09-09如何在 Java 中實(shí)現(xiàn)一個(gè) redis 緩存服務(wù)
為什么要使用緩存?說到底是為了提高系統(tǒng)的運(yùn)行速度。將用戶頻繁訪問的內(nèi)容存放在離用戶最近,訪問速度最快的地方,提高用戶的響應(yīng)速度。下面我們來一起深入學(xué)習(xí)一下吧2019-06-06java生成圖片驗(yàn)證碼返回base64圖片信息方式
這篇文章主要介紹了java生成圖片驗(yàn)證碼返回base64圖片信息方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08解決springboot中@DynamicUpdate注解無效的問題
這篇文章主要介紹了解決springboot中@DynamicUpdate注解無效的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07