Timer 定時器相信都不會陌生,之所以拿它來做源碼分析,是發現整個控制流程可以體現很多有意思的東西。
在業務開發中經常會遇到執行一些簡單定時任務的需求,通常為了避免做一些看起來復雜的控制邏輯,一般考慮使用 Timer 來實現定時任務的執行,下面先給出一個最簡單用法的例子:
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
// scheduledExecutionTime() 返回此任務最近開始執行的時間
Date date = new Date(this.scheduledExecutionTime());
System.out.println("timeTask run " + date);
}
};
// 從現在開始每間隔 1000 ms 計劃執行一個任務
timer.schedule(timerTask, 0, 1000);
Timer 概述
Timer 可以按計劃執行重復的任務或者定時執行指定任務,這是因為 Timer 內部利用了一個后臺線程 TimerThread 有計劃地執行指定任務。
Timer:是一個實用工具類,該類用來調度一個線程(schedule a thread),使它可以在將來某一時刻執行。 Java 的 Timer 類可以調度一個任務運行一次或定期循環運行。 Timer tasks should complete quickly. 即定時器中的操作要盡可能花費短的時間。
TimerTask:一個抽象類,它實現了 Runnable 接口。我們需要擴展該類以便創建自己的 TimerTask ,這個 TimerTask 可以被 Timer 調度。
一個 Timer 對象對應的是單個后臺線程,其內部維護了一個 TaskQueue,用于順序執行計時器任務 TimeTask 。
Timer 中優先隊列的實現
TaskQueue 隊列,內部用一個 TimerTask[] 數組實現優先隊列(二叉堆),默認最大任務數是 128 ,當添加定時任務超過當前最大容量時會這個數組會拓展到原來 2 倍。
優先隊列主要目的是為了找出、返回并刪除優先隊列中最小的元素,這里優先隊列是通過數組實現了平衡二叉堆,TimeQueue 實現的二叉堆用數組表示時,具有最小 nextExecutionTime 的 TimerTask 在隊列中為 queue[1] ,所以堆中根節點在數組中的位置是 queue[1] ,那么第 n 個位置 queue[n] 的子節點分別在 queue[2n] 和 queue[2n+1] 。關于優先隊列的數據結構實現,這里推薦一篇文章:數據結構與算法學習筆記 - 優先隊列、二叉堆、左式堆。
按照 TaskQueue 的描述:This class represents a timer task queue: a priority queue of TimerTasks, ordered on nextExecutionTime.
這是一個優先隊列,隊列的優先級按照 nextExecutionTime 進行調度。
也就說 TaskQueue 按照 TimerTask 的 nextExecutionTime 屬性界定優先級,優先級高的任務先出隊列,也就先執行任務調度。
如上圖所示,列舉了優先隊列中部分操作的實現,優先隊列插入和刪除元素的復雜度都是O(logn),所以add, removeMin 和 rescheduleMin
方法的性能都是不錯的。從上圖可以知道,獲取下一個計劃執行任務時,取隊列的頭出列即可,為了減少額外性能消耗,移除隊列頭部元素的操作是先把隊尾元素賦值到隊首后,再把隊尾置空,隊列數量完成減一后進行優先權值操作。再下面看看保證優先隊列最核心的兩個方法fixUp
和fixDown
。
兩個方法的核心思路都是通過向上或向下調整二叉堆中元素所在位置,保持堆的有序性:
fixUp 是將元素值小于父節點的子節點與父節點交換位置,保持堆有序。交換位置后,原來的子節點可能仍然比更上層的父節點小,
所以整個過程需要循環進行。這樣一來,原來的子節點有可能升級為層級更高的父節點,類似于一個輕的物體從湖底往上浮直到達到其重力與浮力相平衡的過程。
fixDown 將元素值大于子節點的父節點與子節點交換位置,交換位置后, 原來的父節點仍然有可能比其下面的子節點大, 所以還需要繼續進行類相同的操作,以便保持堆的有序性。所以整個過程循環進行。 這類似于一個重的物體從湖面下沉到距離湖底的某個位置,直到達到其重力與浮力相平衡為止。
總的來說,就是調整大的元素下沉,小的元素上浮,反復調整后堆頂一直保持是堆中最小的元素,父節點元素要一直小于或等于子節點。
TimerTask 的調度
前面說完 Timer 源碼中優先隊列的實現,下面我們來看看其如果操作優先隊列,實現 TimerTask 的計劃調度的:
Timer 提供了四個構造方法,每個構造方法都啟動了一個后臺線程(默認不是守護線程,除非主動指定)。所以對于每一個 Timer 對象而言,其內部都是對應著單個后臺線程,這個線程用于順序執行優先隊列中所有的計時器任務。
當初始化完成 Timer 后,我們就可以往 Timer 中添加定時任務,然后定時任務就會按照我們設定的時間交由 Timer 取調度執行。Timer 提供了 schedule 方法,該方法依靠多次重載的方式來適應不同的情況,具體如下:
schedule(TimerTask task, Date time):安排在指定的時間執行指定的任務。
schedule(TimerTask task, long delay) :安排在指定延遲后執行指定的任務。
schedule(TimerTask task, Date firstTime, long period) :安排指定的任務在指定的時間開始進行重復的固定延遲執行。
schedule(TimerTask task, long delay, long period) :安排指定的任務從指定的延遲后開始進行重復的固定延遲執行。
scheduleAtFixedRate :,scheduleAtFixedRate 方法與 schedule 相同,只不過他們的側重點不同,區別后面分析。
scheduleAtFixedRate(TimerTask task, Date firstTime, long period):安排指定的任務在指定的時間開始進行重復的固定速率執行。
scheduleAtFixedRate(TimerTask task, long delay, long period):安排指定的任務在指定的延遲后開始進行重復的固定速率執行。
首先來看 schedule(TimerTask task, Date time)
和 schedule(TimerTask task, long delay)
,第一個參數傳入是定時任務的實例,區別在于方法的第二個參數,date 是在指定的時間點,delay 是當前時間延后多少毫秒。這就引出了 Timer 具有的兩個特性:定時(在指定時間點執行任務)和延遲(延遲多少秒后執行任務)。
值得大家注意的是:這里所說時間都是跟系統時間相關的絕對時間,而不是相對時間,基于這點,Timer 對任務的調度計劃和系統時間息息相關,所以它對系統時間的改變非常敏感。
下面在來看看 schedule(TimerTask task, Date time)
和 schedule(TimerTask task, Date firstTime, long period)
的區別。對比方法中新增的 period 參數,period 作用區別在于 Timer 的另一個特性:周期性地執行任務(一次任務結束后,可以每隔個 period 豪秒后再執行任務,如此反復)。
從上面 schedule 的方法重載來看,最終都是調用了 sched(TimerTask task, long time, long period)
方法,只是傳入的參數不同,下面就再來看就看關于 schedule 和 scheduleAtFixedRate 的區別:
從調用方法來看,他們的區別僅僅是傳入 sched 方法 period 參數正負數的差別,所以具體的就要看 sched 方法的實現。
可以看到 sched 方法主要是設置 TimerTask 屬性和狀態,比如
nextExecutionTime 等,然后將任務添加到隊列中。能看出來,設置定時任務 task 屬性時是加了鎖的,而且在添加任務到隊列時,這里使用 Timer 內 TaskQueue 實例作為對象鎖,并且使用 wait 和 notify 方法來通知任務調度。Timer 類可以保證多個線程可以共享單個 Timer 對象而無需進行外部同步,所以 Timer 類是線程安全的。
這里注意區分開: 前面一個 Timer 對象中用于處理任務調度的后臺線程TimerThread 實例和 schedule 方法傳入后被加入到 TaskQueue 的 TimerTask 任務的實例,兩者是不一樣的。
要想知道為 TimerTask 設置屬性和狀態的作用,那就得進一步看看 TimerTask 類的具體實現了。
TimerTask 類是一個抽象類,可以由 Timer 安排為一次執行或重復執行的任務。它有一個抽象方法 run() 方法,用于子類實現 Runnale 接口。可以在 run 方法中寫定時任務的具體業務邏輯。
可以看到下圖中 TimerTask 類中的文檔描述,如果任務是按計劃執行,那么 nextExecutionTime 屬性是指下次任務的執行時間,時間格式是按照 System.currentTimeMillis 返回的。對于需要重復進行的任務,每個任務執行之前會更新這一屬性。
而 period 屬性是用來表示以毫秒為時間單位的重復任務。period 為正值時表示固定速率執行,負值表示固定延遲執行,值 0 表示一個非重復性的任務。
所謂固定速率執行和固定延遲執行,固定延遲指的是定時任務會因為前一個任務的延遲而導致其后面的定時任務延時,而固定速率執行則不會有這個問題,它是直接按照計劃的速率重復執行,不會考慮前面任務是否執行完。
這也是 scheduleAtFixedRate 與 schedule 方法的區別,兩者側重點不同,schedule 方法側重保存間隔時間的穩定,而 scheduleAtFixedRate 方法更加側重于保持執行頻率的穩定。
另外 TimerTask 還有兩個非抽象方法:
- boolean cancel():取消此計時器任務。
- long scheduledExecutionTime():返回此任務最近實際執行的安排執行時間。
說完這些,下面就來看看 Timer 的后臺線程具體是如何調度隊列中的定時任務,可以看到 TimerThread 是持有任務隊列進行操作的,也就具有了任務調度功能了。
下面就來看看后臺線程的 run 方法調用 mainLoop 具體做了什么:
前面說到每個 Timer 對象內部包含一個 TaskQueue 實例,在執行定時任務時,TimerThread 中將這個 taskqueue 對象作為鎖,在任何時刻只能有一個線程執行 TimerTask 。Timer 類為了保證線程安全的,是不需要外部同步機制就可以共享同一個 Timer 對象。
可以看到 Timer 是不會捕獲異常的,如果 TimerTask 拋出的了未檢查異常則會導致 Timer 線程終止,同時 Timer 也不會重新恢復線程的執行,它會錯誤的認為整個 Timer 線程都會取消。同時,已經被安排但尚未執行的 TimerTask 也不會再執行了,新的任務也不能被調度。所以,如果 TimerTask 拋出未檢查的異常,Timer 將會產生無法預料的行為。
注意看計劃安排任務的核心代碼,包括任務計劃執行時間的設置,也有優先隊列保持二叉堆序性地操作。下面代碼很好地體現了 period 屬性作用,period 為正值時表示固定速率執行,負值表示固定延遲執行,值 0 表示一個非重復性的任務。
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
前面提過 Timer 使用 schedule (TimerTask task, Date firstTime, long period) 方法執行的計時器任務可能會因為前一個任務執行時間較長而延時。每一次執行的 task 的計劃時間會隨著前一個 task 的實際時間而發生改變,也就是 scheduledExecutionTime(n+1) = realExecutionTime(n) + periodTime。也就是說如果第 n 個 task 由于某種情況導致這次的執行時間過程,最后導致 systemCurrentTime>= scheduledExecutionTime(n+1),這是第 n+1 個 task 并不會因為到時了而執行,他會等待第 n 個 task 執行完之后再執行,那么這樣勢必會導致 n+2 個的執行時間 scheduledExecutionTime 發生改變。所以
schedule 方法更加注重保存間隔時間的穩定。
而對于 scheduleAtFixedRate(TimerTask task, Date firstTime, long period),在前面也提過 scheduleAtFixedRate 與 schedule 方法的側重點不同,schedule 方法側重保存間隔時間的穩定,而 scheduleAtFixedRate 方法更加側重于保持執行頻率的穩定。在 schedule 方法中會因為前一個任務的延遲而導致其后面的定時任務延時,而 scheduleAtFixedRate 方法則不會,如果第 n 個 task 執行時間過長導致 systemCurrentTime >= scheduledExecutionTime(n+1),則不會做任何等待他會立即執行第 n+1 個 task,所以 scheduleAtFixedRate 方法執行時間的計算方法不同于 schedule,而是 scheduledExecutionTime(n)=firstExecuteTime +n*periodTime,該計算方法永遠保持不變。所以 scheduleAtFixedRate 更加側重于保持執行頻率的穩定。
說完了 Timer 的源碼分析,相信大致上也能明白定時集整個流程是怎樣的。下面根據上面這些內容,說一些實際使用建議。
使用建議
最近使用阿里 Java 開發編碼規約插件,可以看到提示是建議使用 ScheduledExecutorService 代替 Timer :
那為什么要使用 ScheduledExecutorService 代替 Timer :
前面我們也有提到,Timer 是基于絕對時間的,對系統時間比較敏感,而 ScheduledThreadPoolExecutor 則是基于相對時間;
Timer 是內部是單一線程,而 ScheduledThreadPoolExecutor 內部是個線程池,所以可以支持多個任務并發執行。
Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。
使用 ScheduledExecutorService 更容易明確任務實際執行策略,更方便自行控制。
默認 Timer 執行線程不是 daemon 線程, 任務執行完,主線程(或其他啟動定時器的線程)結束時,task 線程并沒有結束。需要注意潛在內存泄漏問題
下面給出一個實際使用 ScheduledExecutorService 代替 Timer 的例子:
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* ImprovedTimer 改進過的定時器
* 多線程并行處理定時任務時,Timer運行多個TimeTask時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,
* 使用ScheduledExecutorService則沒有這個問題。
*
* @author baishixian
* @date 2017/10/16
*
*/
public class ImprovedTimer {
/**
* 線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:
* 1)newFixedThreadPool和newSingleThreadExecutor:
* ??主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。
* 2)newCachedThreadPool和newScheduledThreadPool:
* ??主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。
*
* 線程池能按時間計劃來執行任務,允許用戶設定計劃執行任務的時間,int類型的參數是設定
* 線程池中線程的最小數目。當任務較多時,線程池可能會自動創建更多的工作線程來執行任務
*/
private final ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new ImprovedTimer.DaemonThreadFactory());
private ScheduledFuture<?> improvedTimerFuture = null;
public ImprovedTimer() {
}
/**
* 周期性重復執行定時任務
* @param command 執行 Runnable
* @param initialDelay 單位 MILLISECONDS
* @param period 單位 MILLISECONDS
*/
public void schedule(Runnable command, long initialDelay, long period){
// initialDelay 毫秒后開始執行任務,以后每隔 period 毫秒執行一次
// schedule方法被用來延遲指定時間來執行某個指定任務。
// 如果你需要周期性重復執行定時任務可以使用scheduleAtFixedRate或者scheduleWithFixedDelay方法,它們不同的是前者以固定頻率執行,后者以相對固定頻率執行。
// 不管任務執行耗時是否大于間隔時間,scheduleAtFixedRate和scheduleWithFixedDelay都不會導致同一個任務并發地被執行。
// 唯一不同的是scheduleWithFixedDelay是當前一個任務結束的時刻,開始結算間隔時間,如0秒開始執行第一次任務,任務耗時5秒,任務間隔時間3秒,那么第二次任務執行的時間是在第8秒開始。
improvedTimerFuture = executorService.scheduleAtFixedRate(command, initialDelay, period, TimeUnit.MILLISECONDS);
}
/**
* 周期性重復執行定時任務
* @param command 執行 Runnable
* @param initialDelay 單位 MILLISECONDS
*/
public void schedule(Runnable command, long initialDelay){
// initialDelay 毫秒后開始執行任務
improvedTimerFuture = executorService.schedule(command, initialDelay, TimeUnit.MILLISECONDS);
}
private void cancel() {
if (improvedTimerFuture != null) {
improvedTimerFuture.cancel(true);
improvedTimerFuture = null;
}
}
public void shutdown() {
cancel();
executorService.shutdown();
}
/**
* 守護線程工廠類,用于生產后臺運行線程
*/
private static final class DaemonThreadFactory implements ThreadFactory {
private AtomicInteger atoInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable);
thread.setName("schedule-pool-Thread-" + atoInteger.getAndIncrement());
thread.setDaemon(true);
return thread;
}
}
}
參考:
詳解 Java 定時任務
Java多線程總結(3)— Timer 和 TimerTask深入分析
OVER...