timer在JDK里面,是很早的一個API了。具有延時的,并具有周期性的任務,在newScheduledThreadPool出來之前我們一般會用Timer和TimerTask來做,但是Timer存在一些缺陷,為什么這么說呢?
創(chuàng)新互聯(lián)建站是一家專業(yè)從事網(wǎng)站建設、成都網(wǎng)站制作、網(wǎng)頁設計的品牌網(wǎng)絡公司。如今是成都地區(qū)具影響力的網(wǎng)站設計公司,作為專業(yè)的成都網(wǎng)站建設公司,創(chuàng)新互聯(lián)建站依托強大的技術實力、以及多年的網(wǎng)站運營經(jīng)驗,為您提供專業(yè)的成都網(wǎng)站建設、營銷型網(wǎng)站建設及網(wǎng)站設計開發(fā)服務!
Timer只創(chuàng)建唯一的線程來執(zhí)行所有Timer任務。如果一個timer任務的執(zhí)行很耗時,會導致其他TimerTask的時效準確性出問題。例如一個TimerTask每10秒執(zhí)行一次,而另外一個TimerTask每40ms執(zhí)行一次,重復出現(xiàn)的任務會在后來的任務完成后快速連續(xù)的被調(diào)用4次,要么完全“丟失”4次調(diào)用。Timer的另外一個問題在于,如果TimerTask拋出未檢查的異常會終止timer線程。這種情況下,Timer也不會重新回復線程的執(zhí)行了;它錯誤的認為整個Timer都被取消了。此時已經(jīng)被安排但尚未執(zhí)行的TimerTask永遠不會再執(zhí)行了,新的任務也不能被調(diào)度了。
這里做了一個小的 demo 來復現(xiàn)問題,代碼如下:
package com.hjc; import java.util.Timer; import java.util.TimerTask; /** * Created by cong on 2018/7/12. */ public class TimerTest { //創(chuàng)建定時器對象 static Timer timer = new Timer(); public static void main(String[] args) { //添加任務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); //添加任務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); } }
如上代碼先添加了一個任務在 500ms 后執(zhí)行,然后添加了第二個任務在 1s 后執(zhí)行,我們期望的是當?shù)谝粋€任務輸出 ---one Task--- 后等待 1s 后第二個任務會輸出 ---two Task---,
但是執(zhí)行完畢代碼后輸出結果如下所示:
例子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(); //啟動一個調(diào)度任務,1S鐘后執(zhí)行 timer.schedule(task,1000); //啟動一個調(diào)度任務,3S鐘后執(zhí)行 timer.schedule(task1,3000); } }
上面程序我們預想是第一個任務執(zhí)行后,第二個任務3S后執(zhí)行的,即輸出一個1000,一個3000.
實際運行結果如下:
實際運行結果并不如我們所愿。世界結果,是過了4S后才輸出第二個任務,即4001約等于4秒。那部分時間時間到哪里去了呢?那個時間是被我們第一個任務的sleep所占用了。
現(xiàn)在我們在第一個任務中去掉Thread.sleep();這一行代碼,運行是否正確了呢?運行結果如下:
可以看到確實是第一個任務過了1S后執(zhí)行,第二個任務在第一個任務執(zhí)行完后過3S執(zhí)行了。
這就說明了Timer只創(chuàng)建唯一的線程來執(zhí)行所有Timer任務。如果一個timer任務的執(zhí)行很耗時,會導致其他TimerTask的時效準確性出問題。
Timer 實現(xiàn)原理分析
下面簡單介紹下 Timer 的原理,如下圖是 Timer 的原理模型介紹:
1.其中 TaskQueue 是一個平衡二叉樹堆實現(xiàn)的優(yōu)先級隊列,每個 Timer 對象內(nèi)部有唯一一個 TaskQueue 隊列。用戶線程調(diào)用 timer 的 schedule 方法就是把 TimerTask 任務添加到 TaskQueue 隊列,在調(diào)用 schedule 的方法時候 long delay 參數(shù)用來說明該任務延遲多少時間執(zhí)行。
2.TimerThread 是具體執(zhí)行任務的線程,它從 TaskQueue 隊列里面獲取優(yōu)先級最小的任務進行執(zhí)行,需要注意的是只有執(zhí)行完了當前的任務才會從隊列里面獲取下一個任務而不管隊列里面是否有已經(jīng)到了設置的 delay 時間,一個 Timer 只有一個 TimerThread 線程,所以可知 Timer 的內(nèi)部實現(xiàn)是一個多生產(chǎn)者單消費者模型。
從實現(xiàn)模型可以知道要探究上面的問題只需看 TimerThread 的實現(xiàn)就可以了,TimerThread 的 run 方法主要邏輯源碼如下:
public void run() { try { mainLoop(); } finally { // 有人殺死了這個線程,表現(xiàn)得好像Timer已取消 synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // 消除過時的引用 } } } private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; //從隊列里面獲取任務時候要加鎖 synchronized(queue) { ...... } if (taskFired) task.run();//執(zhí)行任務 } catch(InterruptedException e) { } } }
可知當任務執(zhí)行過程中拋出了除 InterruptedException 之外的異常后,唯一的消費線程就會因為拋出異常而終止,那么隊列里面的其他待執(zhí)行的任務就會被清除。所以 TimerTask 的 run 方法內(nèi)最好使用 try-catch 結構 catch 主可能的異常,不要把異常拋出到 run 方法外。
其實要實現(xiàn)類似 Timer 的功能使用 ScheduledThreadPoolExecutor 的 schedule 是比較好的選擇。ScheduledThreadPoolExecutor 中的一個任務拋出了異常,其他任務不受影響的。
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(); } }
運行結果如下:
之所以 ScheduledThreadPoolExecutor 的其他任務不受拋出異常的任務的影響是因為 ScheduledThreadPoolExecutor 中的 ScheduledFutureTask 任務中 catch 掉了異常,但是在線程池任務的 run 方法內(nèi)使用 catch 捕獲異常并打印日志是最佳實踐。