細說java.util.Timer,java.util.timer
Timer是用於管理在後台執行的延遲任務或週期性任務,其中的任務使用java.util.TimerTask表示。任務的執行方式有兩種:
- 按固定速率執行:即scheduleAtFixedRate的兩個重載方法
- 按固定順延強制:即schedule的4個重載方法
具體差別會在後面詳細說明。
我們要實現一個定時任務,只需要實現TimerTask的run方法即可。每一個任務都有下一次執行時間nextExecutionTime(毫秒),如果是周期性的任務,那麼每次執行都會更新這個時間為下一次的執行時間,當nextExecutionTime小於目前時間時,都會執行它。
一、使用方式
Timer的具體使用方法非常簡單,比如:
Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("Timer is running"); } }, 2000);上面這個定時任務表示在2秒後開始執行,只執行一次。當然還可以執行循環性任務,只需要添加schedule的第三個參數period,如:
Timer timer = new Timer(); timer. scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println("Timer is running"); } }, 2000, 5000);表示2秒後開始執行,然後每隔5秒執行一次。
二、具體實現的分析
對於每一個Timer,後台只使用一個線程來執行所有的任務。而所有的任務都儲存到一個任務隊列java.util.TaskQueue中,它是Timer的一個內部類,這是一個優先順序隊列,使用的演算法是最小二元堆積(binary min heap)。
Timer的文檔中說:
After the last live reference to a Timer object goes away and all outstanding tasks have completed execution, the timer's task execution thread terminates gracefully (and becomes subject to garbage collection).
意思是(沒有完全按照原文翻譯):當任務隊列中所有的任務都執行完後,即沒有一次性執行的任務,也沒有周期性的任務,那麼Timer的後台線程將優雅地終止,並成為記憶體回收的對象。
但是在測試時卻發現,在執行完一個一次性任務後,任務線程卻不終止,而是一直阻塞,這不知道是何原因,使用的JDK是1.7.0_79。
使用jstack查看到這個線程的狀態如下:
"Timer-0" prio=5 tid=0x00007ff184046000 nid=0x5807 in Object.wait() [0x000000011ebea000] java.lang.Thread.State: WAITING (on object monitor)at java.lang.Object.wait(Native Method)- waiting on <0x00000007ab1bbea0> (a java.util.TaskQueue)at java.lang.Object.wait(Object.java:503)at java.util.TimerThread.mainLoop(Timer.java:526)- locked <0x00000007ab1bbea0> (a java.util.TaskQueue)at java.util.TimerThread.run(Timer.java:505)
可以看到Timer任務線程處理阻塞狀態。
一個TimerTask有四種狀態:
- VIRGIN:新建立的任務的狀態,表示還未安排執行
- SCHEDULED:已安排執行,對於非周期性的任務來說,表示還未執行,當把任務添加到任務執行隊列時的狀態,即調用Timer.schedule時
- EXECUTED:表示非週期性任務已經執行或正在執行中,並且還未被取消
- CANCELLED:表示任務已經取消,當調用cancel方法後即為該狀態,該狀態的任務會在每次執行時被移出隊列
下面看看當調用Timer.schedule時發生了什麼,所有調用Timer.schedule都是調用的一個私人方法sched。
比如順延強制的任務(非週期性任務):
public void schedule(TimerTask task, long delay) { // 延遲不能小於0 if (delay < 0) throw new IllegalArgumentException("Negative delay.”); // 調用私人的sched方法,注意這裡的執行時間是絕對時間,而period為0表示不是週期性任務 sched(task, System.currentTimeMillis()+delay, 0); } 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) { // 如果要執行的任務狀態不是VIRGIN,那麼拋出異常 if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled”); // 設定任務的執行時間,注意這個時間是絕對時間 task.nextExecutionTime = time; // 設定任務執行的周期 task.period = period; // 設定任務的狀態為SCHEDULED task.state = TimerTask.SCHEDULED; } // 把任務添加到任務隊列,等任務執行線程調試執行 queue.add(task); // 檢查下一個將要執行的任務是不是當前添加的任務,如果是則通知背景工作線程 if (queue.getMin() == task) queue.notify(); } }執行任務的後台線程,Timer使用了一個內部類TimerThread,它有一個newTasksMayBeScheduled屬性工作表示是否還能執行任務,預設為true,當調用cancel方法時設定為false,之後將不能再添加或執行任務。同時它還持有一個任務隊列的引用,而不是直接引用Timer的任務隊列,這是因為為了防止循環參考導致Timer無法被記憶體回收,以及任務線程不能正常終止。
下面看一下這個任務線程的實現:
public void run() { try { mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } }
實際上是調用的mainLoop方法:
private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; synchronized(queue) { // 等待任務隊列變成非空,如果任務隊列為空白並且Timer未取消,就阻塞線程,等待任務隊列為非空,從前面可以看到,每次添加完一個任務後,會通知這個線程來檢查執行相應的任務 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)) { // 如果不是非週期性任務,那麼從任務隊列中移除該任務,並把該任務的狀態設定為EXECUTED if (task.period == 0) { // Non-repeating, remove queue.removeMin(); task.state = TimerTask.EXECUTED; } else { // Repeating task, reschedule // 如果是週期性任務,則重新設定任務的下一次執行時間,這裡要注意period有可能為正和負 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) { } } }
其中重新設定任務的下一次執行時間的邏輯:
queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period);
這裡任務執行循環可為正和負:
- 正數:表示按照固定的速率調度執行,比如執行循環是每5秒執行一次,如上一次執行時間是20:51:30,那麼下一次執行時間就為20:51:35,如果由於執行其他任務的時間超過5秒,比如用了15秒,那麼這有可能會導致這種任務不能夠在指定的時間執行,這就破壞了任務執行的速率(rate),但是會在後面連續執行3次。
- 負數:表示按照固定的延遲來調度執行,比如執行循環是每5秒執行一次,在正常情況下,如它的執行時間是20:51:30,但是由於執行其他任務時間花了8秒秒,即執行到當前任務時是20:51:38,那麼它的下一次執行時間將向後延遲,即20:51:43。
三、Timer的缺陷
1、由於執行任務的線程只有一個,所以如果某個任務的執行時間過長,那麼將破壞其他任務的定時精確性。如一個任務每1秒執行一次,而另一個任務執行一次需要5秒,那麼如果是固定速率的任務,那麼會在5秒這個任務執行完成後連續執行5次,而固定延遲的任務將丟失4次執行。
2、如果執行某個任務過程中拋出了異常,那麼執行線程將會終止,導致Timer中的其他任務也不能再執行。
3、Timer使用的是絕對時間,即是某個時間點,所以它執行依賴系統的時間,如果系統時間修改了的話,將導致任務可能不會被執行。
四、更好的替代方法
由於Timer存在上面說的這些缺陷,在JDK1.5中,我們可以使用ScheduledThreadPoolExecutor來代替它,使用Executors.newScheduledThreadPoolFactory 方法或使用ScheduledThreadPoolExecutor的建構函式來建立定時任務,它是基於線程池的實現,不會存在Timer存在的上述問題,當線程數量為1時,它相當於Timer。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。