【定时任务】JDK java.util.Timer定时器的实现原理
在程序中簡單實用Timer的方法,參考學習。
定時任務,也叫定時器,是指在指定的時間執行指定任務的機制,類似于Windows自帶的計劃任務。JDK中提供了定時器的支持—java.util.Timer,下面我們來系統學習一下它的實現原理。
Timer主要由三個部分組成: 任務TimerTask、任務隊列TaskQueue和 任務調試者TimerThread。多個任務單元 TimerTask按照一定的優先級組成了任務隊列TaskQueue,任務調度TimerThread按照一定的規則每次取出任務隊列中的一個任務進行處理。
TimerTask 任務單元
//*********任務狀態*****************//VIRGIN表示Task剛剛被創建static final int VIRGIN = 0;//************幾個狀態常量******************//SCHEDULED表示Task已經被加入TaskQueue中,等待調度static final int SCHEDULED = 1;//EXECUTED表示Task已經被執行static final int EXECUTED = 2;//CANCELLED表示Task已經被取消//**************兩個重要的成員變量******************//nextExecutionTime這個成員變量用到記錄該任務下次執行時間, 其格式和System.currentTimeMillis()一致.這個值是作為任務隊列中任務排序的依據. 任務調試者執行每個任務前會對這個值作處理,重新計算下一次任務執行時間,并為這個變量賦值.long nextExecutionTime; //period 用來描述任務的執行方式: 0表示不重復執行的任務. 正數表示固定速率執行的任務. 負數表示固定延遲執行的任務. (固定速率: 不考慮該任務上一次執行情況,始終從開始時間算起的每period執行下一次。固定延遲: 考慮該任務一次執行情況,在上一次執行后period執行下一次)。long period = 0;TaskQueue任務隊列
TaskQueue是用來保存TimerTask的隊列,是一個數組, 采用平衡二叉堆來實現優先級調度, 并且是一個最小堆, 這個堆中queue[n] 的孩子是queue[2*n] 和 queue[2*n+1]。任務隊列的優先級按照TimerTask類的成員變量nextExecutionTime值來排序。在任務隊列中, nextExecutionTime最小就是所有任務中最早要被調度來執行的(也就是堆頂的Task), 所以被安排在queue[1] (假設任務隊列非空),對于堆中任意一個節點m,和他的任意子孫節點n,一定遵循: m.nextExecutionTime <= n.nextExecutionTime.
下面是TaskQueue的核心代碼,也是最小堆的實現代碼:
添加任務:
/***首先會判斷是否已經滿了,如果已經滿了, 那么容量擴大至原來2倍, 然后將需要添加的任務放到隊列最后. 之后就會調用fixUp 方法來進行隊列中任務優先級調整.*/ void add(TimerTask task) { if (size + 1 == queue.length) queue = Arrays.copyOf(queue, 2 * queue.length); queue[++size] = task; fixUp(size); } /*** fixUp方法的作用是盡量將隊列中指定位置(k)的任務向隊列前面移動, 即提高它的優先級. 因為新加入的方法很有可能比已經在任務隊列中的其它任務要更早執行.*/ private void fixUp(int k) { while (k > 1) { int j = k >> 1; // 對于正數,右移位 <==> j = k/2, 所以j的位置就是k的父親節點 if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } }總結:這個過程可以這個描述: 不斷地將k位置上元素和它的父親進行比較, 如果發現孩子節點比父親小的時候, 那么將父親和孩子位置互換. 直到最小的來到隊列第一個位置。
移除任務:
/*** 首先直接將當前任務隊列中最后一個任務賦給queue[1], 然后將隊列中任務數量--, 最后和上面類似, 但是這里是調用fixDown(int k)方法了, 盡量將k位置的任務向隊列后面移動.*/ void removeMin() { queue[1] = queue[size]; queue[size--] = null; // Drop extra reference to prevent memory leak fixDown(1); } /** * 將k位置的元素向堆底方向移動* 1. j = k << 1, .<br> * 2. 將 j 精確定位到較小的兒子.<br> * 3. 然后k與j比較,如果k大于j的話, 那么互換< * 4.繼續... */ private void fixDown(int k) { int j; // 如果還沒有到隊列的最后,并且沒有溢出( j > 0 ),在沒有出現溢出的情況下, j = k << 1 等價于 j = 2 * k,將j定位到兒子中 while ((j = k << 1) <= size && j > 0) { // 找到k的兩個孩子中小的那個. if (j < size && queue[j].nextExecutionTime > queue[j + 1].nextExecutionTime) j++; // 找到這個較小的孩子后,(此時k是父親,j是較小的兒子),父親和兒子互換位置,即k和j換位子.這樣一直下去就可以將這個較大的queue[1]向下堆底移動了. if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime) break; TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp; k = j; } }TimerThread任務調度
TimerThread就是用來調度TaskQueue中的任務的線程。關于任務調度主要有一個成員變量 newTasksMayBeScheduled和調度方法mainLoop()。
boolean newTasksMayBeScheduled = true; private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired = false; synchronized (queue) { while (queue.isEmpty() && newTasksMayBeScheduled) { queue.wait(); } if (queue.isEmpty()) break; // 直接挑出mainLoop了. long currentTime, executionTime; task = queue.getMin(); // 獲取這個任務隊列第一個任務 synchronized (task.lock) { if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; } currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; if (taskFired = (executionTime <= currentTime)) { if (task.period == 0) { // period表示不重復,移除出隊列 queue.removeMin(); task.state = TimerTask.EXECUTED; } else { //重復執行的設置重新調度 queue.rescheduleMin(task.period < 0 ? currentTime - task.period : executionTime + task.period); } } }//釋放鎖 if (!taskFired) queue.wait(executionTime - currentTime); } if (taskFired) // Task fired; run it, holding no locks task.run(); } catch (InterruptedException e) { } }// while(true) }newTasksMayBeScheduled變量用來表示是否需要繼續等待新任務了。默認情況這個變量是true , 并且這個變量一直是true的。只有兩種情況的時候會變成 false :
1.當調用Timer的cancel方法
2.沒有引用指向Timer對象了.
任務調度mainLoop()方法中的一個while可以理解為一次任務調度:
STEP 1 : 判斷任務隊列中是否還有任務, 如果任務隊列為空了, 但是newTasksMayBeScheduled變量還是true, 表明 需要繼續等待新任務, 所以一直等待。
STEP 2 : 等待喚醒后, 再次判斷隊列中是否有任務. 如果還是沒有任務,那么直接結束定時器工作了.因為queue只在兩個地方被調用: addTask和cancel 1.向任務隊列中增加任務會喚醒 2.timer.cancel()的時候也會喚醒. 那么這里如果還是empty,那么就是cancel的喚醒了,所以可以結束timer工作了。
STEP 3 : 從任務隊列中取出第一個任務,即nextExecutionTime最小的那個任務。
STEP 4: 判斷這個任務是否已經被取消. 如果已經被取消了,那么就直接從任務隊列中移除這個任務(removeMin() ),然后直接進入下一個任務調度周期。
STEP 5 : 判斷是否到了或者已經超過了這個任務應該執行的時間了。如果到了 , 不會立即執行它,而是會在這次循環的最后來執行它。
這里做的事情可以看作是為下一個調度周期進行準備:包括:
如果還沒有到執行時間 , 一直等到 queue.wait(executionTime - currentTime),并且等待完畢后,似乎可以開始運行了, 但是這里設計成不立即運行,而是直接進入下一個任務調度周期.(因為taskFired =false,所以不會在這次進行執行的.)
STEP6: 開始調用任務的run方法運行任務。
還有一點需要注意:
在step2中我們學習到,TimerThread的調度核心是起一個while循環,不斷檢查是否有task需要執行,其中兩次調用了queue.wait()方法。1.向任務隊列中增加任務會喚醒 2.timer.cancel()的時候也會喚醒,兩種情況下queue.notify()方法會被調用。
但是是否上面兩種情況調用notify就已經足夠了?當queue為空,并且沒人調用add或cancel方法時,TimerThread永遠都不會stop。不用擔心,對于這個地方的處理JDK加上了一種比較保險的方法:
/*** This object causes the timer's task execution thread to exit* gracefully when there are no live references to the Timer object and no* tasks in the timer queue. It is used in preference to a finalizer on* Timer as such a finalizer would be susceptible to a subclass's* finalizer forgetting to call it.*/ private final Object threadReaper = new Object() {protected void finalize() throws Throwable {synchronized(queue) {thread.newTasksMayBeScheduled = false;queue.notify(); // In case queue is empty.}} };用到了Object對象的finalize方法,大家都知道finalize方法是對象被GC的時候調用的。上述做法的思路是:當一個Timer已經沒有任何對象引用時,自然不會有新的Task加入到隊列中,Timer對象自然也就會被垃圾回收,此時TimerThread也就應該stop了,所以在垃圾回收的時候還應該把newTasksMayBeScheduled設置為false,并且喚起正在wait的TimerThread線程。所以說,如果你創建的Timer不再需要了,最好是調用cancel接口手動取消,否則的話TimerThread就需要等到垃圾回收的時候才會stop。
總結
以上是生活随笔為你收集整理的【定时任务】JDK java.util.Timer定时器的实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Java线程】线程池的原理和实现
- 下一篇: 【Hibernate】Hibernate