本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
本節探討定時任務,定時任務的應用場景是很是多的,好比:java
在Java中,有兩種方式實現定時任務:git
它們的基本用法都是比較簡單的,但若是對它們沒有足夠的瞭解,則很容易陷入其中的一些陷阱,下面,咱們就來介紹它們的用法、原理以及那些坑。github
TimerTask表示一個定時任務,它是一個抽象類,實現了Runnable,具體的定時任務須要繼承該類,實現run方法。編程
Timer是一個具體類,它負責定時任務的調度和執行,它有以下主要方法:swift
//在指定絕對時間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)的區別,都是重複執行,但後一次任務執行相對的時間是不同的,對於固定延時,它是基於上次任務的"實際"執行時間來算的,若是因爲某種緣由,上次任務延時了,則本次任務也會延時,而固定頻率會盡可能補夠運行次數。bash
另外,須要注意的是,若是第一次計劃執行的時間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的基本使用是比較簡單的,但咱們須要注意:
因爲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。
在併發應用程序中,通常咱們應該儘可能利用高層次的服務,好比前面章節介紹的各類併發容器、任務執行服務和線程池等,避免本身管理線程和它們之間的同步,但在個別狀況下,本身管理線程及同步是必需的,這時,除了利用前面章節介紹的synchronized, wait/notify, 顯示鎖和條件等基本工具,Java併發包還提供了一些高級的同步和協做工具,以方便實現併發應用,讓咱們下一節來了解它們。
(與其餘章節同樣,本節全部代碼位於 github.com/swiftma/pro…)
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。