電腦程式的思維邏輯 (80) - 定時任務的那些坑

来源:http://www.cnblogs.com/swiftma/archive/2017/04/18/6724894.html
-Advertisement-
Play Games

本節,我們來探討Java中的定時任務Timer和ScheduledExecutorService,它們的基本用法都是比較簡單的,但如果對它們缺乏足夠的瞭解,則很容易墜入其中的一些坑,都有哪些坑呢? ...


本節探討定時任務,定時任務的應用場景是非常多的,比如:

  • 鬧鐘程式或任務提醒,指定時間叫床或在指定日期提醒還信用卡
  • 監控系統,每隔一段時間採集下系統數據,對異常事件報警
  • 統計系統,一般凌晨一定時間統計昨日的各種數據指標

在Java中,有兩種方式實現定時任務:

  • 使用java.util包中的Timer和TimerTask
  • 使用Java併發包中的ScheduledExecutorService

它們的基本用法都是比較簡單的,但如果對它們沒有足夠的瞭解,則很容易陷入其中的一些陷阱,下麵,我們就來介紹它們的用法、原理以及那些坑。

Timer和TimerTask

基本用法

TimerTask表示一個定時任務,它是一個抽象類,實現了Runnable,具體的定時任務需要繼承該類,實現run方法。

Timer是一個具體類,它負責定時任務的調度和執行,它有如下主要方法: 

//在指定絕對時間time運行任務task
public void schedule(TimerTask task, Date time)
//在當前時間延時delay毫秒後運行任務task
public void schedule(TimerTask task, long delay)
//固定延時重覆執行,第一次計劃執行時間為firstTime,後一次的計劃執行時間為前一次"實際"執行時間加上period
public void schedule(TimerTask task, Date firstTime, long period)
//同樣是固定延時重覆執行,第一次執行時間為當前時間加上delay
public void schedule(TimerTask task, long delay, long period)
//固定頻率重覆執行,第一次計劃執行時間為firstTime,後一次的計劃執行時間為前一次"計劃"執行時間加上period
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
//同樣是固定頻率重覆執行,第一次計劃執行時間為當前時間加上delay
public void scheduleAtFixedRate(TimerTask task, long delay, long period)

需要註意固定延時(fixed-delay)與固定頻率(fixed-rate)的區別,都是重覆執行,但後一次任務執行相對的時間是不一樣的,對於固定延時,它是基於上次任務的"實際"執行時間來算的,如果由於某種原因,上次任務延時了,則本次任務也會延時,而固定頻率會儘量補夠運行次數。

另外,需要註意的是,如果第一次計劃執行的時間firstTime是一個過去的時間,則任務會立即運行,對於固定延時的任務,下次任務會基於第一次執行時間計算,而對於固定頻率的任務,則會從firstTime開始算,有可能加上period後還是一個過去時間,從而連續運行很多次,直到時間超過當前時間。

我們通過一些簡單的例子具體來看下。

基本示例

看一個最簡單的例子:

public class BasicTimer {
    static class DelayTask extends TimerTask {
        
        @Override
        public void run() {
            System.out.println("delayed task");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new DelayTask(), 1000);
        Thread.sleep(2000);
        timer.cancel();
    }
}

創建一個Timer對象,1秒鐘後運行DelayTask,最後調用Timer的cancel方法取消所有定時任務。

看一個固定延時的簡單例子:

public class TimerFixedDelay {

    static class LongRunningTask extends TimerTask {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
            }
            System.out.println("long running finished");
        }
    }

    static class FixedDelayTask extends TimerTask {
        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();

        timer.schedule(new LongRunningTask(), 10);
        timer.schedule(new FixedDelayTask(), 100, 1000);
    }
}

有兩個定時任務,第一個運行一次,但耗時5秒,第二個是重覆執行,1秒一次,第一個先運行。運行該程式,會發現,第二個任務只有在第一個任務運行結束後才會開始運行,運行後1秒一次。

如果替換上面的代碼為固定頻率,即代碼變為:

public class TimerFixedRate {

    static class LongRunningTask extends TimerTask {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
            }
            System.out.println("long running finished");
        }
    }

    static class FixedRateTask extends TimerTask {

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();

        timer.schedule(new LongRunningTask(), 10);
        timer.scheduleAtFixedRate(new FixedRateTask(), 100, 1000);
    }
}

運行該程式,第二個任務同樣只有在第一個任務運行結束後才會運行,但它會把之前沒有運行的次數補過來,一下子運行5次,輸出類似下麵這樣:

long running finished
1489467662330
1489467662330
1489467662330
1489467662330
1489467662330
1489467662419
1489467663418

基本原理

Timer內部主要由兩部分組成,任務隊列和Timer線程。任務隊列是一個基於堆實現的優先順序隊列,按照下次執行的時間排優先順序。Timer線程負責執行所有的定時任務,需要強調的是,一個Timer對象只有一個Timer線程,所以,對於上面的例子,任務才會被延遲。

Timer線程主體是一個迴圈,從隊列中拿任務,如果隊列中有任務且計劃執行時間小於等於當前時間,就執行它,如果隊列中沒有任務或第一個任務延時還沒到,就睡眠。如果睡眠過程中隊列上添加了新任務且新任務是第一個任務,Timer線程會被喚醒,重新進行檢查。

在執行任務之前,Timer線程判斷任務是否為周期任務,如果是,就設置下次執行的時間並添加到優先順序隊列中,對於固定延時的任務,下次執行時間為當前時間加上period,對於固定頻率的任務,下次執行時間為上次計劃執行時間加上period。

需要強調是,下次任務的計劃是在執行當前任務之前就做出了的,對於固定延時的任務,延時相對的是任務執行前的當前時間,而不是任務執行後,這與後面講到的ScheduledExecutorService的固定延時計算方法是不同的,後者的計算方法更合乎一般的期望。

另一方面,對於固定頻率的任務,它總是基於最先的計劃計劃的,所以,很有可能會出現前面例子中一下子執行很多次任務的情況。

死迴圈

一個Timer對象只有一個Timer線程,這意味著,定時任務不能耗時太長,更不能是無限迴圈,看個例子:

public class EndlessLoopTimer {
    static class LoopTask extends TimerTask {

        @Override
        public void run() {
            while (true) {
                try {
                    // ... 執行任務
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 永遠也沒有機會執行
    static class ExampleTask extends TimerTask {
        @Override
        public void run() {

            System.out.println("hello");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new LoopTask(), 10);
        timer.schedule(new ExampleTask(), 100);
    }
}

第一個定時任務是一個無限迴圈,其後的定時任務ExampleTask將永遠沒有機會執行。

異常處理

關於Timer線程,還需要強調非常重要的一點,在執行任何一個任務的run方法時,一旦run拋出異常,Timer線程就會退出,從而所有定時任務都會被取消。我們看個簡單的示例:

public class TimerException {

    static class TaskA extends TimerTask {
        
        @Override
        public void run() {
            System.out.println("task A");
        }
    }
    
    static class TaskB extends TimerTask {
        
        @Override
        public void run() {
            System.out.println("task B");
            throw new RuntimeException();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();
        timer.schedule(new TaskA(), 1, 1000);
        timer.schedule(new TaskB(), 2000, 1000);
    }
}

期望TaskA每秒執行一次,但TaskB會拋出異常,導致整個定時任務被取消,程式終止,屏幕輸出為:

task A
task A
task B
Exception in thread "Timer-0" java.lang.RuntimeException
    at laoma.demo.timer.TimerException$TaskB.run(TimerException.java:21)
    at java.util.TimerThread.mainLoop(Timer.java:555)
    at java.util.TimerThread.run(Timer.java:505)

所以,如果希望各個定時任務不互相干擾,一定要在run方法內捕獲所有異常。

小結

可以看到,Timer/TimerTask的基本使用是比較簡單的,但我們需要註意:

  • 背後只有一個線程在運行
  • 固定頻率的任務被延遲後,可能會立即執行多次,將次數補夠
  • 固定延時任務的延時相對的是任務執行前的時間
  • 不要在定時任務中使用無限迴圈
  • 一個定時任務的未處理異常會導致所有定時任務被取消

ScheduledExecutorService   

介面和類定義

由於Timer/TimerTask的一些問題,Java併發包引入了ScheduledExecutorService,它是一個介面,其定義為:

public interface ScheduledExecutorService extends ExecutorService {
    //單次執行,在指定延時delay後運行command
    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
    //單次執行,在指定延時delay後運行callable
    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
    //固定頻率重覆執行
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
    //固定延時重覆執行
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
}

它們的返回類型都是ScheduledFuture,它是一個介面,擴展了Future和Delayed,沒有定義額外方法。這些方法的大部分語義與Timer中的基本是類似的。對於固定頻率的任務,第一次執行時間為initialDelay後,第二次為initialDelay+period,第三次initialDelay+2*period,依次類推。不過,對於固定延時的任務,它是從任務執行後開始算的,第一次為initialDelay後,第二次為第一次任務執行結束後再加上delay。與Timer不同,它不支持以絕對時間作為首次運行的時間。

ScheduledExecutorService的主要實現類是ScheduledThreadPoolExecutor,它是線程池ThreadPoolExecutor的子類,是基於線程池實現的,它的主要構造方法是:

public ScheduledThreadPoolExecutor(int corePoolSize) 

此外,還有構造方法可以接受參數ThreadFactory和RejectedExecutionHandler,含義與ThreadPoolExecutor一樣,我們就不贅述了。

它的任務隊列是一個無界的優先順序隊列,所以最大線程數對它沒有作用,即使corePoolSize設為0,它也會至少運行一個線程。

工廠類Executors也提供了一些方便的方法,以方便創建ScheduledThreadPoolExecutor,如下所示:

//單線程的定時任務執行服務
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
//多線程的定時任務執行服務
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)

基本示例

由於可以有多個線程執行定時任務,一般任務就不會被某個長時間運行的任務所延遲了,比如,對於前面的TimerFixedDelay,如果改為:

public class ScheduledFixedDelay {
    static class LongRunningTask implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
            }
            System.out.println("long running finished");
        }
    }

    static class FixedDelayTask implements Runnable {
        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService timer = Executors.newScheduledThreadPool(10);
        timer.schedule(new LongRunningTask(), 10, TimeUnit.MILLISECONDS);
        timer.scheduleWithFixedDelay(new FixedDelayTask(), 100, 1000,
                TimeUnit.MILLISECONDS);
    }
}

再次執行,第二個任務就不會被第一個任務延遲了。

另外,與Timer不同,單個定時任務的異常不會再導致整個定時任務被取消了,即使背後只有一個線程執行任務,我們看個例子:

public class ScheduledException {

    static class TaskA implements Runnable {

        @Override
        public void run() {
            System.out.println("task A");
        }
    }

    static class TaskB implements Runnable {

        @Override
        public void run() {
            System.out.println("task B");
            throw new RuntimeException();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService timer = Executors
                .newSingleThreadScheduledExecutor();
        timer.scheduleWithFixedDelay(new TaskA(), 0, 1, TimeUnit.SECONDS);
        timer.scheduleWithFixedDelay(new TaskB(), 2, 1, TimeUnit.SECONDS);
    }
}

TaskA和TaskB都是每秒執行一次,TaskB兩秒後執行,但一執行就拋出異常,屏幕的輸出類似如下:

task A
task A
task B
task A
task A
...

這說明,定時任務TaskB被取消了,但TaskA不受影響,即使它們是由同一個線程執行的。不過,需要強調的是,與Timer不同,沒有異常被拋出來,TaskB的異常沒有在任何地方體現。所以,與Timer中的任務類似,應該捕獲所有異常。

基本原理

ScheduledThreadPoolExecutor的實現思路與Timer基本是類似的,都有一個基於堆的優先順序隊列,保存待執行的定時任務,它的主要不同是:

  • 它的背後是線程池,可以有多個線程執行任務
  • 它在任務執行後再設置下次執行的時間,對於固定延時的任務更為合理
  • 任務執行線程會捕獲任務執行過程中的所有異常,一個定時任務的異常不會影響其他定時任務,但發生異常的任務也不再被重新調度,即使它是一個重覆任務

小結

本節介紹了Java中定時任務的兩種實現方式,Timer和ScheduledExecutorService,需要特別註意Timer的一些陷阱,實踐中建議使用ScheduledExecutorService。

它們的共同局限是,不太勝任複雜的定時任務調度,比如,每周一和周三晚上18:00到22:00,每半小時執行一次。對於類似這種需求,可以利用我們之前在32節33節介紹的日期和時間處理方法,或者利用更為強大的第三方類庫,比如Quartz(http://www.quartz-scheduler.org/)。

在併發應用程式中,一般我們應該儘量利用高層次的服務,比如前面章節介紹的各種併發容器、任務執行服務線程池等,避免自己管理線程和它們之間的同步,但在個別情況下,自己管理線程及同步是必需的,這時,除了利用前面章節介紹的synchronized, wait/notify, 顯示鎖條件等基本工具,Java併發包還提供了一些高級的同步和協作工具,以方便實現併發應用,讓我們下一節來瞭解它們。

(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic)

----------------

未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心原創,保留所有版權。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 迴文數的判定 很多人糾結迴文數的數學概念 忘了可以直接分割字元串 如此連中文都可以分割 純給萌新看 ...
  • 效果圖: ...
  • public function getContract($value=''){ App::uses ( 'UserContractController', 'Controller' ); $Contract = new UserContractController(); $pdfs = $Contr ...
  • 核心開發介面介紹1、hibernate_0500_CoreAPI2、HiberanteAPI 文檔需要單獨下載 線上api鏈接:http://docs.jboss.org/hibernate/core/3.3/api3、Configuration a) AnnotationConfiguration ...
  • 關於這個問題我糾結了很久,每次打開網頁yii\db\Connection::open幾乎都耗時1000ms。 其實這個問題很好解決:只要把config\db.php配置信息里的localhost,改成ip地址就好,可能是地址解析的原因才會耗時那麼久。 ...
  • 在之前我們使用Swift的Perfect框架來開發服務端程式時,聊到了Perfect中的路由配置。而在SpringMVC中的路由配置與其也是大同小異的。說到路由,其實就是將URL映射到Java的具體類中的具體方法,或者映射到具體的JSP文件上。本篇博客主要就闡述瞭如何在SpringMVC中配置路由以 ...
  • 1、簡介 Apached的重寫功能,即是mod_rewrite模塊功能,它是apache的一個模塊。它的功能非常強大,可以操作URL中的所有部分。 因此我們就可以改寫url,給用戶提供一個簡介大方的url,當用戶訪問時可以通過mod_rewrite模塊功能轉換為真正的資源路徑。通過mod_rewri ...
  • PHP常用字元串的操作函數 字元串轉換類函數 addcslashes函數:以C語言風格使用反斜線轉義字元串中的字元 addslashes函數:使用反斜線引用字元串 chop函數:清除字元串中的連續空格 get_html_translation_table函數:返回htmlspecialchars() ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...