日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

为什么你不该用Timer

發布時間:2025/6/15 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 为什么你不该用Timer 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

概述

在Java開發中,用過定時功能的同學一定不會對Timer感到陌生。不過,除了Timer,在Java 5之后又引入了一個定時工具ScheduledThreadPoolExecutor,那么我們應該如何在這兩個定時工具之間進行選擇呢?

一般情況下我們都建議使用ScheduledThreadPoolExecutor而不是Timer,主要原因有以下3點:

  • Timer使用的是絕對時間,系統時間的改變會對Timer產生一定的影響;而ScheduledThreadPoolExecutor使用的是相對時間,所以不會有這個問題。
  • Timer使用單線程來處理任務,長時間運行的任務會導致其他任務的延時處理,而ScheduledThreadPoolExecutor可以自定義線程數量。
  • Timer沒有對運行時異常進行處理,一旦某個任務觸發運行時異常,會導致整個Timer崩潰,而ScheduledThreadPoolExecutor對運行時異常做了捕獲(可以在afterExecute()回調方法中進行處理),所以更加安全。
  • 下面我們就來通過了解Timer與ScheduledThreadPoolExecutor的運行原理來理解上面幾個問題出現的原因。

    Timer的運行機制

    • TimerTask:任務類。內部持有nextExecutionTime變量,表示任務實際執行時間點,單位為毫秒,使用System.currentTimeMillis() + delay計算得出。
    • TimerQueue:使用小根堆實現的優先隊列。按照TimerTask的實際執行時間點由小到大排序。
    • TimerThread:顧名思義,這是實際執行任務的線程。

    TimerThread會在Timer初始化后啟動,之后會進入mainLoop()方法,該方法會不斷從TimerQueue中取出時間點最小的TimerTask。如果該TimerTask的執行時間點已到,則直接調用TimerTask.run()執行;否則,調用wait()方法,等待相應的時間。

    而我們調用Timer.schedule()方法,實際上是通過TimerQueue.add()方法,將TimerTask加入任務等待隊列。

    這里還有一個需要注意的地方是:當加入任務的執行時間點是優先隊列中最小的時,就調用notify()方法喚醒TimerThread,而TimerThread在被喚醒后會重新調用TimerQueue.getMin()方法,再次調用wait(),不過這次的等待時間就變成了新加入任務的時間點。

    ScheduledThreadPoolExecutor的運行機制

    ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,對線程池的原理不了解的同學,可以看一下我的這篇文章:從零實現ImageLoader(三)—— 線程池詳解。

    ScheduledThreadPoolExecutor的實現比Timer要復雜一些,不過要是理解了線程池的運行原理,其實也不難。它只不過是在ThreadPoolExecutor的基礎上使用自定義的阻塞隊列DelayedWorkQueue來實現任務定時功能。所以ScheduledThreadPoolExecutor的運行流程其實和ThreadPoolExecutor是差不多的。

    • ScheduledFutureTask:任務類。內部持有time變量,單位為納秒,通過System.nanoTime() + delay計算得出。
    • DelayedWorkQueue:使用小根堆實現的優先阻塞隊列,將ScheduledFutureTask按照從小到大的順序排列,同時在take()方法內實現阻塞操作。
    • WorkerThread:這里為了簡單起見,我將線程池的核心線程和臨時線程統一寫成WorkerThread,但需要注意的是ScheduledThreadPoolExecutor是線程池的一個子類,所以線程池的那一套東西在ScheduledThreadPoolExecutor里也是有的。

    光從這兩個圖上看,好像ScheduledThreadPoolExecutor和Timer的實現都大同小異,不過是換了一些名字,但實際上這兩個的實現還是有很大的不同的,不止因為ScheduledThreadPoolExecutor使用的是多線程。

    在Timer里定時功能的實現主要依靠TimerThread.mainLoop()的等待,而ScheduledThreadPoolExecutor使用的是多線程,在每個線程里都單獨實現定時功能是不現實的,因此,ScheduledThreadPoolExecutor將定時功能放在了DelayedWorkQueue類里,而由于DelayedWorkQueue是阻塞隊列,所以定時任務的實現實際上就在DelayedWorkQueue.take()方法中。下面我們就來分析一下DelayedWorkQueue.take()到底做了什么。

    Leader/Follower模式

    在多線程網絡編程中,我們一般使用一個線程監聽端口,在接收到事件后再使用其他的線程去完成操作。這種情況下,在兩個線程之間的上下文切換開銷其實是很大的,于是我們有了Leader/Follower模式:

    在Leader/Follower模式中,不存在一個專門用來監聽的線程,所有的線程都是等價的,而這些線程會不斷在Leader、Follower和Processor這三個狀態之間來回切換。

    在程序中會保證每個時刻有且只有一個Leader,這個Leader就暫時充當了之前用來監聽端口線程的作用。而當有一個新的事件發生時,Leader不再是重新找一個線程去處理連接,而是自己轉化為Processor處理事件,并且重新指定一個Follower作為新的Leader。當事件處理完畢后,Processor又會轉化為Follower等待重新成為Leader。

    take()方法的原理

    這里的take()方法就借助了Leader/Follower模式的思想,同一時刻只有一個Leader線程,不過這里由于任務執行的時間點是已經確定了的,所以不再是等待一個觸發事件,而是等待最小任務所對應的延遲時間。其他的Follower線程則處于無限等待的狀態,直到當前Leader到達指定時間后轉化為Processor去處理任務,這時就會喚醒一個Follower作為下一任的Leader。而Processor在處理完任務后又會重新加入Follower進行等待。

    絕對時間與相對時間

    了解了Timer與ScheduledThreadPoolExecutor的運行機制,下面我們就來看一下Timer的這些缺陷究竟是怎么回事。

    首先是絕對時間與相對時間的問題,可能有人已經發現,不管是TimerTask還是ScheduledFutureTask都是存儲的實際執行時間點,只不過一個是毫秒,一個是納秒,難道時間單位還會對這些有影響?確實,時間單位是不會對任務的執行有影響的,不過這里的玄機就在于這個時間的計算方式:System.currentTimeMillis()與System.nanoTime()。

    System.currentTimeMillis()大家已經很清楚了,就是當前時間與1970年1月1日午夜的時間差的毫秒數,而System.nanoTime()又是什么呢?官方文檔里是這么說的:

    此方法只能用于測量已過的時間,與系統或鐘表時間的其他任何時間概念無關。返回值表示從某一固定但任意的時間算起的毫微秒數。

    這就是Timer與ScheduledThreadPoolExecutor一個是基于絕對時間而另一個是基于相對時間的原因。下面我們寫個例子來測試一下:

    public static void main(String[] args) {System.out.println("Start:\t" + new Date());Executors.newSingleThreadScheduledExecutor().schedule(() -> {System.out.println("Executor:\t" + new Date());}, 60, TimeUnit.SECONDS);new Timer().schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Timer:\t" + new Date());}}, 60000); }復制代碼

    輸出:

    Start: Sun Oct 08 10:51:44 CST 2017 Executor: Sun Oct 08 10:51:41 CST 2017 Timer: Sun Oct 08 10:52:45 CST 2017復制代碼

    這里,我在啟動之后將系統的時鐘向后調了一分鐘,所以實際的啟動時間應該是10:50:44,由于ScheduledThreadPoolExecutor的等待時間與系統無關,所以在一分鐘后執行;而Timer是基于絕對時間的所以在10:52:45執行,實際上這時已經過去兩分鐘了。

    單線程與多線程

    Timer的第二個缺陷是,由于它使用的是單線程,所以長時間執行的任務會對其他任務產生影響。

    public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());ScheduledExecutorService service = Executors.newScheduledThreadPool(3);service.schedule(() -> {System.out.println("Executor 任務1:\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}, 60, TimeUnit.SECONDS);service.schedule(() -> {System.out.println("Executor 任務2:\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}, 60, TimeUnit.SECONDS);Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Timer 任務1:\t\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}}, 60000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Timer 任務2:\t\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}}, 60000); }復制代碼

    輸出:

    Start: Sun Oct 08 11:10:34 CST 2017 Executor 任務1: Sun Oct 08 11:11:34 CST 2017 Executor 任務2: Sun Oct 08 11:11:34 CST 2017 Timer 任務1: Sun Oct 08 11:11:34 CST 2017 Timer 任務2: Sun Oct 08 11:12:04 CST 2017復制代碼

    可以看到ScheduledThreadPoolExecutor中的兩個任務在等待一分鐘之后同時執行;而在Timer中的任務2卻因任務1長達半分鐘的執行時間,總共等了一分半鐘才得以執行。

    異常處理

    最后我們來看一下Timer與ScheduledThreadPoolExecutor對異常的處理情況:

    Timer

    Timer內部沒有對異常做任何處理,如果任務執行發生運行時異常,整個TimerThread都會崩潰:

    public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {throw new RuntimeException("Timer 任務1");}}, 60000);timer.schedule(new TimerTask() {@Overridepublic void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Timer 任務2:\t\t" + new Date());}}, 60000); }復制代碼

    輸出:

    Start: Sun Oct 08 11:53:05 CST 2017 Exception in thread "Timer-0" java.lang.RuntimeException: Timer 任務1at main.Main$1.run(Main.java:32)at java.util.TimerThread.mainLoop(Timer.java:555)at java.util.TimerThread.run(Timer.java:505)復制代碼

    可以看到,任務1拋出的運行時異常導致整個Timer線程崩潰,任務2自然也沒有執行。

    ScheduledThreadPoolExecutor

    ScheduledThreadPoolExecutor中對異常的處理實際上是ThreadPoolExecutor類完成的,ThreadPoolExecutor在任務運行時對異常做了捕獲,并且將異常傳入了afterExecute()方法:

    public class ThreadPoolExecutor extends AbstractExecutorService {final void runWorker(Worker w) {...Throwable thrown = null;try {task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}...} }復制代碼

    我們來驗證一下:

    public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();service.schedule(() -> {throw new RuntimeException("Executor 任務1");}, 60, TimeUnit.SECONDS);service.schedule(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Executor 任務2:\t" + new Date());}, 60, TimeUnit.SECONDS); }復制代碼

    輸出:

    Start: Sun Oct 08 11:33:35 CST 2017 Executor 任務2: Sun Oct 08 11:34:36 CST 2017復制代碼

    可以看到這里雖然任務1拋出了運行時異常,但由于線程池內部完善的異常處理機制,任務2得以成功執行。

    后記

    看了這么多Timer的缺陷,你還在猶豫嗎?趕快放棄Timer,投入ScheduledThreadPoolExecutor的懷抱吧!

    總結

    以上是生活随笔為你收集整理的为什么你不该用Timer的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。