JDK自帶的java.util.Timer定時器的實現原理
文章分類:Java編程
本文是我在項目中使用Timer後, 又看了一下Timer實現原理的整理. 主要介紹JKD自帶的java.util.Timer定時器的實現原理. Timer使用本身很簡單, 同樣, 他的設計原理也很精妙.
如果你僅僅只是想知道如何在自己的程序中來使用java.util.Timer的一些方法,那麼請移步:
http://robinsoncrusoe.iteye.com/blog/986320
轉載請註明: http://nileader.iteye.com/blog/1049253
API介紹在這裏: http://online.chinaitpower.com/api/jdk150/java/util/Timer.html
Timer和TimerTask Since JDK1.3
Timer中最主要由三個部分組成:
任務 TimerTask 、 任務隊列: TaskQueue queue 和 任務調試者:TimerThread thread
他們之間的關係可以通過下面圖示:
在這個圖中,可以清楚地看到這Timer本身及其和這三個部分的關係:
1. Timer可以看作是面向開發人員的一個"接口"
2. 所有向Timer添加的任務都會被放入一個TaskQueue類型的任務隊列中去.(如何安排任務優先級順序下文會講)
3. 任務調度由TimerThread負責.
任務單元 TimerTask
首先看一下任務單元實體類: TimerTask.
在這個類中, 要關注的是任務狀態和幾個狀態常量:
- /** 標識任務的狀態 */
- int state = VIRGIN;
- /** 任務的狀態的常量 */
- static final int VIRGIN = 0 ;
- static final int SCHEDULED = 1 ;
- static final int EXECUTED = 2 ;
- static final int CANCELLED = 3 ;
以及一個比較重要的兩個成員變量:
- long nextExecutionTime;
- long period = 0 ;
nextExecutionTime 這個成員變量用到記錄該任務下次執行時間, 其格式和System.currentTimeMillis()一致.
這個值是作爲任務隊列中任務排序的依據. 任務調試者執行每個任務前會對這個值作處理,重新計算下一次任務執行時間,併爲這個變量賦值.
period 用來描述任務的執行方式: 0表示不重複執行的任務. 正數表示固定速率執行的任務. 負數表示固定延遲執行的任務.
(固定速率: 不考慮該任務上一次執行情況,始終從開始時間算起的每period執行下一次. 固定延遲: 考慮該任務一次執行情況,在上一次執行後period執行下一次).
任務隊列 TaskQueue
事實上任務隊列是一個數組, 採用平衡二叉堆來實現他的優先級調度, 並且是一個小頂堆. 需要注意的是, 這個堆中queue[n] 的孩子是queue[2*n] 和 queue[2*n+1].
任務隊列的優先級按照TimerTask類的成員變量nextExecutionTime值來排序(注意, 這裏的任務指的是那些交由定時器來執行的, 繼承TimerTask的對象).
在任務隊列中, nextExecutionTime最小就是所有任務中最早要被調度來執行的, 所以被安排在queue[1] (假設任務隊列非空).
對於堆中任意一個節點n, 和他的任意子孫節點d,一定遵循: n.nextExecutionTime <= d.nextExecutionTime.
[添加任務]
- void add(TimerTask task) {
- if (size + 1 == queue.length)
- queue = Arrays.copyOf(queue, 2 * queue.length);
- queue[++size] = task;
- fixUp(size);
- }
首先會判斷是否已經滿了,(任務隊列的初始容量是128 ),如果已經滿了, 那麼容量擴大至原來2倍, 然後將需要添加的任務放到隊列最後.
之後就會調用fixUp 方法來進行隊列中任務優先級調整. 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位置上元素和它的父親進行比較, 上文也提到過了. 由於必須滿足 "對於堆中任意一個節點n, 和他的任意子孫節點d,一定遵循: n.nextExecutionTime <= d.nextExecutionTime.", 那麼在不斷比較過程中, 如果發現孩子節點比父親小的時候, 那麼將父親和孩子位置互換. 直到來到隊列第一個位置.
[移除任務]
- void removeMin() {
- queue[1 ] = queue[size];
- queue[size--] = null ; // Drop extra reference to prevent memory leak
- fixDown(1 );
- }
從任務隊列中移除一個任務的過程, 首先直接將當前任務隊列中最後一個任務賦給queue[1], 然後將隊列中任務數量--, 最後和上面類似, 但是這裏是調用fixDown(int k)方法了, 儘量將k位置的任務向隊列後面移動.
- /**
- * -將k位置的元素向堆底方向移動.<br>
- * 1. j = k << 1, 將j定位到兒子中.<br>
- * 2. 將 j 精確定位到較小的兒子.<br>
- * 3. 然後k與j比較,如果k大於j的話, 那麼互換<br>
- * 4.繼續...
- */
- private void fixDown( int k) {
- int j;
- // 如果還沒有到隊列的最後,並且沒有溢出( j > 0 )
- // 在沒有出現溢出的情況下, j = k << 1 等價於 j = 2 * k ;
- while ((j = k << 1 ) <= size && j > 0 ) {
- // 找到k的兩個孩子中小的那個.
- if (j < size && queue[j].nextExecutionTime > queue[j + 1 ].nextExecutionTime)
- j++; // j indexes smallest kid
- // 找到這個較小的孩子後,(此時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
關於任務調度主要要講下一個成員變量 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 ) { // 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)
- 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 : 判斷是否到了或者已經超過了這個任務應該執行的時間了.
如果到了
, 不會立即執行它,而是會在這次循環的最後來執行它.
這裏做的事情可以看作是爲下一個調度週期進行準備:包括:
1. 判斷是否是重複(repeating)任務,如果 task.period == 0, 那麼就不是重複任務,所以可以直接將這個任務從任務隊列中移除了(removeMin()
),因爲沒有必要留到下一個調度週期中去了.
2. 如果是需要重複執行的任務, 那麼就要重新設置這個任務的nextExecutionTime,即調用方法queue.rescheduleMin(long)
,這個方法中會調用fixDown(1)
負責重新調整任務隊列的優先級順序.
如果還沒有到執行時間 , 一直等到 queue.wait(executionTime - currentTime)
並且等待完畢後,似乎可以開始運行了, 但是這裏設計成不立即運行,而是直接進入下一個任務調度週期.(因爲taskFired =false,所以不會在這次進行執行的.)
STEP: 6 開始調用任務的run方法運行任務.