Java中的Timer與TimerTask源碼及使用解析
一. Timer
在Java中,經(jīng)常使用Timer來定時調(diào)度任務。
Timer 調(diào)度任務有一次性調(diào)度和循環(huán)調(diào)度,循環(huán)調(diào)度有分為固定速率調(diào)度(fixRate)和固定時延調(diào)度(fixDelay)。固定速率就好比你今天加班到很晚,但是到了第二天還必須準點到公司上班,如果你一不小心加班到了第二天早上 9 點,你就連休息的時間都沒有了。而固定時延的意思是你必須睡夠 8 個小時再過來上班,如果你加班到凌晨 6 點,那就可以下午過來上班了。固定速率強調(diào)準點,固定時延強調(diào)間隔。
Timer timer = new Timer(); TimerTask timerTask = new TimerTask() { @Override public void run() { // TODO Auto-generated method stub } }; //延遲1秒后開始調(diào)度任務 timer.schedule(timerTask, 1000); //延遲1秒,固定延遲1秒周期性調(diào)度 timer.schedule(timerTask, 1000, 1000); //延遲1秒,固定速率1秒周期性調(diào)度 timer.scheduleAtFixedRate(timerTask, 1000, 1000);
如果你有一個任務必須每天準點調(diào)度,那就應該使用固定速率調(diào)度,并且要確保每個任務執(zhí)行時間不要太長,千萬別超過了第二天這個點。如果你有一個任務需要每隔幾分鐘跑一次,那就使用固定時延調(diào)度,它不是很在乎你的單個任務要跑多長時間。
二. 內(nèi)部源碼
Timer 類里包含一個任務隊列和一個異步輪訓線程。任務隊列里容納了所有待執(zhí)行的任務,所有的任務將會在這一個異步線程里執(zhí)行,切記任務的執(zhí)行代碼不可以拋出異常,否則會導致 Timer 線程掛掉,所有的任務都沒得執(zhí)行了。單個任務也不易執(zhí)行時間太長,否則會影響任務調(diào)度在時間上的精準性。比如你一個任務跑了太久,其它等著調(diào)度的任務就一直處于饑餓狀態(tài)得不到調(diào)度。所有任務的執(zhí)行都是這單一的 TimerThread 線程。
/** * The timer task queue. This data structure is shared with the timer * thread. The timer produces tasks, via its various schedule calls, * and the timer thread consumes, executing timer tasks as appropriate, * and removing them from the queue when they're obsolete. */ private final TaskQueue queue = new TaskQueue(); //調(diào)度隊列 /** * The timer thread. */ private final TimerThread thread = new TimerThread(queue); //輪詢線程
Timer 的任務隊列 TaskQueue 是一個特殊的隊列,它內(nèi)部是一個數(shù)組。這個數(shù)組會按照待執(zhí)行時間進行堆排序,堆頂元素總是待執(zhí)行時間最小的任務。輪訓線程會每次輪訓出時間點最近的并且到點的任務來執(zhí)行。數(shù)組會自動擴容,如果任務非常多。
private TimerTask[] queue = new TimerTask[128]; //增加調(diào)度任務時,如果長度超過隊列長度,則擴容為原先2倍 void add(TimerTask task) { // Grow backing store if necessary if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2*queue.length); queue[++size] = task; fixUp(size); } private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); // Constrain value of period sufficiently to prevent numeric // overflow while still being effectively infinitely large. if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) { //任意線程均可修改調(diào)度隊列,因此會導致現(xiàn)場不安全,所以這邊用同步。 if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { //調(diào)度隊列增加任務時,其他線程也可以修改該任務,導致線程不安全,所以這邊用同步 if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); //增加到調(diào)度隊列中 if (queue.getMin() == task) queue.notify(); } }
三. 任務狀態(tài)
TimerTask 有 4 個狀態(tài),VIRGIN 是默認狀態(tài),剛剛實例化還沒有被調(diào)度。SCHEDULED 表示已經(jīng)將任務塞進 TaskQueue 等待被執(zhí)行。EXECUTED 表示任務已經(jīng)執(zhí)行完成。CANCELLED 表示任務被取消了,還沒來得及執(zhí)行就被人為取消了。
/** * The state of this task, chosen from the constants below. */ int state = VIRGIN; //默認狀態(tài)是VIRGIN /** * This task has not yet been scheduled. */ static final int VIRGIN = 0; /** * This task is scheduled for execution. If it is a non-repeating task, * it has not yet been executed. */ static final int SCHEDULED = 1; /** * This non-repeating task has already executed (or is currently * executing) and has not been cancelled. */ static final int EXECUTED = 2; /** * This task has been cancelled (with a call to TimerTask.cancel). */ static final int CANCELLED = 3; /** * Next execution time for this task in the format returned by * System.currentTimeMillis, assuming this task is scheduled for execution. * For repeating tasks, this field is updated prior to each task execution. */ long nextExecutionTime; //下次調(diào)度執(zhí)行時間 /** * Period in milliseconds for repeating tasks. A positive value indicates * fixed-rate execution. A negative value indicates fixed-delay execution. * A value of 0 indicates a non-repeating task. */ long period = 0; //間隔
對于一個循環(huán)任務來說,它不存在 EXECUTED 狀態(tài),因為它每次剛剛執(zhí)行完成,就被重新調(diào)度了。EXECUTED 狀態(tài)僅僅存在于一次性任務,而且這個狀態(tài)其實并不是表示任務已經(jīng)執(zhí)行完成,它是指已經(jīng)從任務隊列里摘出來了,馬上就要執(zhí)行。
任務間隔字段 period 比較特殊,當使用固定速率時,period 為正值,當使用固定間隔時,period 為負值,當任務是一次性時,period 為零。
public void schedule(TimerTask task, long delay) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); sched(task, System.currentTimeMillis()+delay, 0); } public void schedule(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, -period); } public void scheduleAtFixedRate(TimerTask task, long delay, long period) { if (delay < 0) throw new IllegalArgumentException("Negative delay."); if (period <= 0) throw new IllegalArgumentException("Non-positive period."); sched(task, System.currentTimeMillis()+delay, period); }
對于固定速率來說,如果任務執(zhí)行時間太長超出了間隔,那么它可能會持續(xù)霸占任務隊列,因為它的調(diào)度時間將總是低于 currentTime,排在堆頂,每次輪訓取出來的都是它。運行完畢后,重新調(diào)度這個任務,它的時間依舊趕不上。持續(xù)下去你會看到這個任務的調(diào)度時間遠遠落后于當前時間,而其它任務可能會徹底餓死。這就是為什么一定要特別注意固定速率的循環(huán)任務運行時間不宜過長。
四. 任務鎖
Timer 的任務支持取消操作,取消任務的線程和執(zhí)行任務的線程極有可能不是一個線程。有可能任務正在執(zhí)行中,結(jié)果另一個線程表示要取消任務。這時候 Timer 是如何處理的呢?在 TimerTask 類里看到了一把鎖。當任務屬性需要修改的時候,都會加鎖。
public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); queue.notify(); // In case queue was already empty. } } private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); // Constrain value of period sufficiently to prevent numeric // overflow while still being effectively infinitely large. if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) { if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); if (queue.getMin() == task) queue.notify(); } } private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // Wait for queue to become non-empty while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // Queue is empty and will forever remain; die // Queue nonempty; look at first evt and do the right thing long currentTime, executionTime; task = queue.getMin(); synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } 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); } } } if (!taskFired) // Task hasn't yet fired; wait queue.wait(executionTime - currentTime); } if (taskFired) // Task fired; run it, holding no locks task.run(); } catch(InterruptedException e) { } } }
在任務運行之前會檢查任務是不是已經(jīng)被取消了,如果取消了,就從隊列中移除。一旦任務開始運行 run(),對于單次任務來說它就無法被取消了,而循環(huán)任務將不會繼續(xù)下次調(diào)度。如果任務沒有機會得到執(zhí)行(時間設(shè)置的太長),那么即使這個任務被取消了,它也會一直持續(xù)躺在任務隊列中。設(shè)想如果你調(diào)度了一系列久遠的任務,然后都取消了,這可能會成為一個內(nèi)存泄露點。所以 Timer 還單獨提供了一個 purge() 方法可以一次性清空所有的已取消的任務。
public int purge() { int result = 0; synchronized(queue) { for (int i = queue.size(); i > 0; i--) { if (queue.get(i).state == TimerTask.CANCELLED) { queue.quickRemove(i); result++; } } if (result != 0) queue.heapify(); } return result; }
五. 任務隊列為空
任務隊列里沒有任務了,調(diào)度線程必須按一定的策略進行睡眠。它需要睡眠一直到最先執(zhí)行的任務到點時立即醒來,所以睡眠截止時間就是第一個任務將要執(zhí)行的時間。同時在睡覺的時候,有可能會有新的任務被添加進來,它的調(diào)度時間可能會更加提前,所以當有新的任務到來時需要可以喚醒正在睡眠的線程。
private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // Wait for queue to become non-empty while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // Queue is empty and will forever remain; die // Queue nonempty; look at first evt and do the right thing long currentTime, executionTime; task = queue.getMin(); synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } 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); } } } if (!taskFired) // Task hasn't yet fired; wait queue.wait(executionTime - currentTime); } if (taskFired) // Task fired; run it, holding no locks task.run(); } catch(InterruptedException e) { } } } private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); // Constrain value of period sufficiently to prevent numeric // overflow while still being effectively infinitely large. if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) { if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); if (queue.getMin() == task) queue.notify(); } }
代碼中的 wait() 方法就是調(diào)用了 Object.wait() 來進行睡眠。當有新任務進來了,發(fā)現(xiàn)這個新任務的運行時間是最早的,那就調(diào)用 notify() 方法喚醒輪訓線程。
六. Timer終止
Timer 提供了 cancel() 方法清空隊列,停止調(diào)度器,不允許有任何新任務進來。它會將 newTasksMayBeScheduled 字段設(shè)置為 false 表示 Timer 即將終止。
public void cancel() { synchronized(queue) { thread.newTasksMayBeScheduled = false; queue.clear(); queue.notify(); // In case queue was already empty. } } private void sched(TimerTask task, long time, long period) { if (time < 0) throw new IllegalArgumentException("Illegal execution time."); // Constrain value of period sufficiently to prevent numeric // overflow while still being effectively infinitely large. if (Math.abs(period) > (Long.MAX_VALUE >> 1)) period >>= 1; synchronized(queue) { if (!thread.newTasksMayBeScheduled) //調(diào)度器已經(jīng)停止則拋出異常 throw new IllegalStateException("Timer already cancelled."); synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } queue.add(task); if (queue.getMin() == task) queue.notify(); } }
我們還注意到 Timer.cancel() 方法會喚醒輪訓線程,為的是可以立即停止輪訓。不過如果任務正在執(zhí)行中,這之后 cancel() 就必須等到任務執(zhí)行完畢才可以停止。
到此這篇關(guān)于Java中的Timer與TimerTask源碼及使用解析的文章就介紹到這了,更多相關(guān)Java中的Timer與TimerTask內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 處理圖片與base64 編碼的相互轉(zhuǎn)換的示例
本篇文章主要介紹了Java 處理圖片與base64 編碼的相互轉(zhuǎn)換的示例,具有一定的參考價值,有興趣的可以了解一下2017-08-08Java 中 String,StringBuffer 和 StringBuilder 的區(qū)別及用法
這篇文章主要介紹了Java 中 String,StringBuffer 和 StringBuilder 的區(qū)別及用法的相關(guān)資料,需要的朋友可以參考下2017-03-03詳解Spring Data操作Redis數(shù)據(jù)庫
Redis是一種NOSQL數(shù)據(jù)庫,Key-Value形式對數(shù)據(jù)進行存儲,其中數(shù)據(jù)可以以內(nèi)存形式存在,也可以持久化到文件系統(tǒng)。Spring data對Redis進行了很好的封裝,用起來也是十分的得心應手,接下來通過本文給大家分享Spring Data操作Redis數(shù)據(jù)庫,需要的朋友參考下2017-03-03java圖的深度優(yōu)先遍歷實現(xiàn)隨機生成迷宮
這篇文章主要為大家詳細介紹了java圖的深度優(yōu)先遍歷實現(xiàn)隨機生成迷宮,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-01-01springboot實現(xiàn)maven多模塊和打包部署
本文主要介紹了springboot實現(xiàn)maven多模塊和打包部署,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-04-04MyBatis的注解使用、ORM層優(yōu)化方式(懶加載和緩存)
這篇文章主要介紹了MyBatis的注解使用、ORM層優(yōu)化方式(懶加載和緩存),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10