Java并發(fā)Timer源碼分析
timer在JDK里面,是很早的一個(gè)API了。具有延時(shí)的,并具有周期性的任務(wù),在newScheduledThreadPool出來(lái)之前我們一般會(huì)用Timer和TimerTask來(lái)做,但是Timer存在一些缺陷,為什么這么說(shuō)呢?
Timer只創(chuàng)建唯一的線程來(lái)執(zhí)行所有Timer任務(wù)。如果一個(gè)timer任務(wù)的執(zhí)行很耗時(shí),會(huì)導(dǎo)致其他TimerTask的時(shí)效準(zhǔn)確性出問(wèn)題。例如一個(gè)TimerTask每10秒執(zhí)行一次,而另外一個(gè)TimerTask每40ms執(zhí)行一次,重復(fù)出現(xiàn)的任務(wù)會(huì)在后來(lái)的任務(wù)完成后快速連續(xù)的被調(diào)用4次,要么完全“丟失”4次調(diào)用。Timer的另外一個(gè)問(wèn)題在于,如果TimerTask拋出未檢查的異常會(huì)終止timer線程。這種情況下,Timer也不會(huì)重新回復(fù)線程的執(zhí)行了;它錯(cuò)誤的認(rèn)為整個(gè)Timer都被取消了。此時(shí)已經(jīng)被安排但尚未執(zhí)行的TimerTask永遠(yuǎn)不會(huì)再執(zhí)行了,新的任務(wù)也不能被調(diào)度了。
這里做了一個(gè)小的 demo 來(lái)復(fù)現(xiàn)問(wèn)題,代碼如下:
package com.hjc; import java.util.Timer; import java.util.TimerTask; /** * Created by cong on 2018/7/12. */ public class TimerTest { //創(chuàng)建定時(shí)器對(duì)象 static Timer timer = new Timer(); public static void main(String[] args) { //添加任務(wù)1,延遲500ms執(zhí)行 timer.schedule(new TimerTask() { @Override public void run() { System.out.println("---one Task---"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } throw new RuntimeException("error "); } }, 500); //添加任務(wù)2,延遲1000ms執(zhí)行 timer.schedule(new TimerTask() { @Override public void run() { for (;;) { System.out.println("---two Task---"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }, 1000); } }
如上代碼先添加了一個(gè)任務(wù)在 500ms 后執(zhí)行,然后添加了第二個(gè)任務(wù)在 1s 后執(zhí)行,我們期望的是當(dāng)?shù)谝粋€(gè)任務(wù)輸出 ---one Task--- 后等待 1s 后第二個(gè)任務(wù)會(huì)輸出 ---two Task---,
但是執(zhí)行完畢代碼后輸出結(jié)果如下所示:
例子2,
public class Shedule { private static long start; public static void main(String[] args) { TimerTask task = new TimerTask() { public void run() { System.out.println(System.currentTimeMillis()-start); try{ Thread.sleep(3000); }catch (InterruptedException e){ e.printStackTrace(); } } }; TimerTask task1 = new TimerTask() { @Override public void run() { System.out.println(System.currentTimeMillis()-start); } }; Timer timer = new Timer(); start = System.currentTimeMillis(); //啟動(dòng)一個(gè)調(diào)度任務(wù),1S鐘后執(zhí)行 timer.schedule(task,1000); //啟動(dòng)一個(gè)調(diào)度任務(wù),3S鐘后執(zhí)行 timer.schedule(task1,3000); } }
上面程序我們預(yù)想是第一個(gè)任務(wù)執(zhí)行后,第二個(gè)任務(wù)3S后執(zhí)行的,即輸出一個(gè)1000,一個(gè)3000.
實(shí)際運(yùn)行結(jié)果如下:
實(shí)際運(yùn)行結(jié)果并不如我們所愿。世界結(jié)果,是過(guò)了4S后才輸出第二個(gè)任務(wù),即4001約等于4秒。那部分時(shí)間時(shí)間到哪里去了呢?那個(gè)時(shí)間是被我們第一個(gè)任務(wù)的sleep所占用了。
現(xiàn)在我們?cè)诘谝粋€(gè)任務(wù)中去掉Thread.sleep();這一行代碼,運(yùn)行是否正確了呢?運(yùn)行結(jié)果如下:
可以看到確實(shí)是第一個(gè)任務(wù)過(guò)了1S后執(zhí)行,第二個(gè)任務(wù)在第一個(gè)任務(wù)執(zhí)行完后過(guò)3S執(zhí)行了。
這就說(shuō)明了Timer只創(chuàng)建唯一的線程來(lái)執(zhí)行所有Timer任務(wù)。如果一個(gè)timer任務(wù)的執(zhí)行很耗時(shí),會(huì)導(dǎo)致其他TimerTask的時(shí)效準(zhǔn)確性出問(wèn)題。
Timer 實(shí)現(xiàn)原理分析
下面簡(jiǎn)單介紹下 Timer 的原理,如下圖是 Timer 的原理模型介紹:
1.其中 TaskQueue 是一個(gè)平衡二叉樹堆實(shí)現(xiàn)的優(yōu)先級(jí)隊(duì)列,每個(gè) Timer 對(duì)象內(nèi)部有唯一一個(gè) TaskQueue 隊(duì)列。用戶線程調(diào)用 timer 的 schedule 方法就是把 TimerTask 任務(wù)添加到 TaskQueue 隊(duì)列,在調(diào)用 schedule 的方法時(shí)候 long delay 參數(shù)用來(lái)說(shuō)明該任務(wù)延遲多少時(shí)間執(zhí)行。
2.TimerThread 是具體執(zhí)行任務(wù)的線程,它從 TaskQueue 隊(duì)列里面獲取優(yōu)先級(jí)最小的任務(wù)進(jìn)行執(zhí)行,需要注意的是只有執(zhí)行完了當(dāng)前的任務(wù)才會(huì)從隊(duì)列里面獲取下一個(gè)任務(wù)而不管隊(duì)列里面是否有已經(jīng)到了設(shè)置的 delay 時(shí)間,一個(gè) Timer 只有一個(gè) TimerThread 線程,所以可知 Timer 的內(nèi)部實(shí)現(xiàn)是一個(gè)多生產(chǎn)者單消費(fèi)者模型。
從實(shí)現(xiàn)模型可以知道要探究上面的問(wèn)題只需看 TimerThread 的實(shí)現(xiàn)就可以了,TimerThread 的 run 方法主要邏輯源碼如下:
public void run() { try { mainLoop(); } finally { // 有人殺死了這個(gè)線程,表現(xiàn)得好像Timer已取消 synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // 消除過(guò)時(shí)的引用 } } } private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; //從隊(duì)列里面獲取任務(wù)時(shí)候要加鎖 synchronized(queue) { ...... } if (taskFired) task.run();//執(zhí)行任務(wù) } catch(InterruptedException e) { } } }
可知當(dāng)任務(wù)執(zhí)行過(guò)程中拋出了除 InterruptedException 之外的異常后,唯一的消費(fèi)線程就會(huì)因?yàn)閽伋霎惓6K止,那么隊(duì)列里面的其他待執(zhí)行的任務(wù)就會(huì)被清除。所以 TimerTask 的 run 方法內(nèi)最好使用 try-catch 結(jié)構(gòu) catch 主可能的異常,不要把異常拋出到 run 方法外。
其實(shí)要實(shí)現(xiàn)類似 Timer 的功能使用 ScheduledThreadPoolExecutor 的 schedule 是比較好的選擇。ScheduledThreadPoolExecutor 中的一個(gè)任務(wù)拋出了異常,其他任務(wù)不受影響的。
ScheduledThreadPoolExecutor 例子如下:
/** * Created by cong on 2018/7/12. */ public class ScheduledThreadPoolExecutorTest { static ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); public static void main(String[] args) { scheduledThreadPoolExecutor.schedule(new Runnable() { public void run() { System.out.println("---one Task---"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } throw new RuntimeException("error "); } }, 500, TimeUnit.MICROSECONDS); scheduledThreadPoolExecutor.schedule(new Runnable() { public void run() { for (int i =0;i<5;++i) { System.out.println("---two Task---"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }, 1000, TimeUnit.MICROSECONDS); scheduledThreadPoolExecutor.shutdown(); } }
運(yùn)行結(jié)果如下:
之所以 ScheduledThreadPoolExecutor 的其他任務(wù)不受拋出異常的任務(wù)的影響是因?yàn)?ScheduledThreadPoolExecutor 中的 ScheduledFutureTask 任務(wù)中 catch 掉了異常,但是在線程池任務(wù)的 run 方法內(nèi)使用 catch 捕獲異常并打印日志是最佳實(shí)踐。
- 使用java.util.Timer實(shí)現(xiàn)任務(wù)調(diào)度
- java Timer測(cè)試定時(shí)調(diào)用及固定時(shí)間執(zhí)行代碼示例
- Android RxJava創(chuàng)建操作符Timer的方法
- Java定時(shí)器Timer使用方法詳解
- java中timer的schedule和scheduleAtFixedRate方法區(qū)別詳解
- Java多線程定時(shí)器Timer原理及實(shí)現(xiàn)
- java定時(shí)器timer的使用方法代碼示例
- Java 中Timer和TimerTask 定時(shí)器和定時(shí)任務(wù)使用的例子
- Java中Timer的用法詳解
相關(guān)文章
JPA merge聯(lián)合唯一索引無(wú)效問(wèn)題解決方案
這篇文章主要介紹了JPA merge聯(lián)合唯一索引無(wú)效問(wèn)題解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09Springboot容器級(jí)后置處理器BeanDefinitionRegistryPostProcessor
這篇文章主要介紹了Springboot容器級(jí)后置處理器BeanDefinitionRegistryPostProcessor,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-01-01簡(jiǎn)單工廠模式_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了簡(jiǎn)單工廠模式的相關(guān)資料,和大家一起學(xué)習(xí)靜態(tài)工廠方法模式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08MyBatis-Plus updateById不更新null值的方法解決
用Mybatis-Plus的updateById()來(lái)更新數(shù)據(jù)時(shí),無(wú)法將字段設(shè)置為null值,更新后數(shù)據(jù)還是原來(lái)的值,本文就來(lái)詳細(xì)的介紹一下解決方法,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08Java8新特性之接口中的默認(rèn)方法和靜態(tài)方法詳解
今天帶大家學(xué)習(xí)的是Java8新特性的相關(guān)知識(shí),文章圍繞著Java接口中的默認(rèn)方法和靜態(tài)方法展開,文中有非常詳細(xì)的的代碼示例,需要的朋友可以參考下2021-06-06Java Benchmark 基準(zhǔn)測(cè)試的實(shí)例詳解
這篇文章主要介紹了Java Benchmark 基準(zhǔn)測(cè)試的實(shí)例詳解的相關(guān)資料,這里提供實(shí)例幫助大家學(xué)習(xí)理解這部分內(nèi)容,需要的朋友可以參考下2017-08-08如何解決EasyExcel導(dǎo)出文件LocalDateTime報(bào)錯(cuò)問(wèn)題
這篇文章主要介紹了如何解決EasyExcel導(dǎo)出文件LocalDateTime報(bào)錯(cuò)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06Java基于JDBC實(shí)現(xiàn)事務(wù),銀行轉(zhuǎn)賬及貨物進(jìn)出庫(kù)功能示例
這篇文章主要介紹了Java基于JDBC實(shí)現(xiàn)事務(wù),銀行轉(zhuǎn)賬及貨物進(jìn)出庫(kù)功能,較為詳細(xì)的分析了事務(wù)操作的原理、實(shí)現(xiàn)方法及java基于jdbc連接數(shù)據(jù)庫(kù)實(shí)現(xiàn)銀行事務(wù)操作的相關(guān)技巧,需要的朋友可以參考下2017-12-12