Timer機制原始碼淺析

天星技術團隊發表於2019-03-27

作者:點先生 時間:2019.3.27

逼逼兩句

Q:定時、延時任務有幾種方式可以實現?
A:Handler、Timer、ScheduledThreadPool、AlarmManager

Handler機制大家應該都爛熟於心了,今天我來講講Timer這個不常被問到的定時器。 改日再說執行緒池,預計是週日。

Timer機制原始碼淺析

Timer機制包含了四個主要核心類:Timer,TaskQueue,TimerThread,TimerTask。我們們一個個來了解。

Timer

Timer類載入時建立新的任務佇列,新的定時器執行緒。並將兩個繫結起來。

public class Timer {
    private final TaskQueue queue = new TaskQueue();
    private final TimerThread thread = new TimerThread(queue);
}
複製程式碼

初始化Timer

Timer機制原始碼淺析

反正就是給thread設定名字,或者設定是否是守護執行緒,最後開啟執行緒;這個thread,就是TimerThread。

Timer機制原始碼淺析

呼叫這四個方法可以執行定時任務延時任務週期執行任務

Timer機制原始碼淺析

這兩個方法與上面最後兩個方法很類似,不同的地方在於sched()的最後一個引數,傳入當前值或是相反數值,這裡的具體影響後面會介紹到。sched()的核心程式碼為:

private void sched(TimerTask task, long time, long period) {
       //其他邏輯
       synchronized(queue) {
          //其他邏輯
          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();
        }
    }
複製程式碼

主要就是將初始化後的task進行賦值,然後加入佇列。 至此Timer裡面就還有兩個方法沒說到。

public void cancel(){ 清空佇列,通知佇列 }
public int purge(){ 將佇列中狀態為“CANCELLED”的任務移除,並重排序佇列。 }
複製程式碼

TaskQueue

Timer機制原始碼淺析

任務佇列實際上就是一個TimerTask的大小為128的陣列。size表示佇列中的任務數。 其他的就是一些操作此陣列的方法

int size() { 獲取當前任務數 }
void add(TimerTask task){ 新增任務到陣列,並 fixUp(size),第一個元素的位置為1,非0。 }
TimerTask getMin(){ 得到最近的一個任務 }
TimerTask get(int i){ 得到i元素 }
void removeMin(){ 移除最近的一個任務,並 fixDown(1) }
void quickRemove(int i){ 快速移速某個任務,不重排序 }
void rescheduleMin(long newTime){ 重新設定最近任務的執行時間,並 fixDown(1) }
boolean isEmpty(){ 判斷佇列是否為空 }
void clear(){ 清空佇列 }
void fixUp(int k){ 排序方法1 }
void fixDown(int k){ 排序方法2 }
void heapify(){ 排序方法3 }
複製程式碼

三種排序方式不再此深探究。在此留下一個疑問,為何第一個任務新增進來給的位置是1,非0;

TimerThread

class TimerThread extends Thread {
    boolean newTasksMayBeScheduled = true;  
    private TaskQueue queue
    TimerThread(TaskQueue queue) {
        this.queue = queue;
    }
    public void run() {
        try { mainLoop();
        } finally {
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear(); 
            }
        }
    }
複製程式碼
  1. TimerThread是一個Thread。
  2. 初始化的時候繫結對應TaskQueen。
  3. TimerThread正常執行時,就一直迴圈取佇列訊息執行任務。當執行緒被殺死,或者其他異常出現時候,便會清空佇列。 tips:newTasksMayBeScheduled 是標誌這當前是否對定時器物件保持引用。當佇列中不再有任務,則為真。

最後來看看mainLoop()中的核心程式碼:

private void mainLoop() {
        while (true) {
            try {
                synchronized(queue) {
                    //其他邏輯
                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        //其他邏輯
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { 
                                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.run();
            } catch(InterruptedException e) {
            }
        }
    }
複製程式碼

執行緒執行起來之後,就一直在取最近的訊息對比當前時間,執行時間到了,就看是否是一次性任務。如果是一次性任務,就更改任務狀態。如果是週期任務,就把給任務設定新的執行時間再入佇列。如果一開始執行時間就沒到,就wait當前佇列。最後根據執行時間是否到達,執行取出來的最近任務。

tips:週期任務重置時間時,有兩種時間,當period<0時currentTime - task.period ,當period>0時executionTime + task.period。根據Timer中sched()和scheduleAtFixedRate()的區別能推斷出,前者程式碼表示,當前任務執行完之後,再進入period時間。後這程式碼表示,當前任務執行開始的時,就進入period時間。

TimerTask

TimerTask是個抽象類,實現了Runnable介面。內部擁有四個屬性,三個方法。

public abstract class TimerTask implements Runnable {
     final Object lock = new Object(); //物件鎖,用於維護執行緒安全;
     int state = VIRGIN;//狀態值
     long nextExecutionTime;//下一次執行的時間
     long period = 0;//週期時間

     static final int VIRGIN = 0;
     static final int SCHEDULED   = 1;
     static final int EXECUTED    = 2;
     static final int CANCELLED   = 3;
}
複製程式碼

VIRGIN :初始化預設值,表達此任務還沒被加入執行佇列。
SCHEDULED :任務被安排準備執行,已加入執行佇列
EXECUTED : 任務正在執行或者已經執行,還沒被取消。
CANCELLED :任務已經被取消

public abstract void run();

 public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }

 public long scheduledExecutionTime() {
        synchronized(lock) {
            return (period < 0 ? nextExecutionTime + period
                               : nextExecutionTime - period);
        }
    }
複製程式碼

繼承TimerTask或者匿名內部類建立都可以實現執行定時任務,將要執行的動作寫在run()裡面即可。 cancel()返回當前任務狀態值是否是SCHEDULED,再將其狀態值改成CANCELLED。 scheduledExecutionTime()返回的是下一次執行的時間。

Timer與Handler

  • Timer機制的結構跟Handler類似,具體處理不一樣,但都分為四大結構。
  • Timer、Handler:主控器
  • TimerTask、Message:訊息/任務
  • TimerQueue、MessageQueue:訊息/任務佇列
  • TimerThread、Looper:迴圈取訊息/任務
優缺點 Handler Timer
執行同一個非週期任務 只需要再發一次訊息 需要建立新的TimerTask
通訊 執行緒間通訊靈活 TimerTask執行在子執行緒中
可靠性 週期執行任務比較可靠 週期執行任務不可靠(下面解釋)
記憶體洩漏 容易洩漏 容易洩漏
記憶體消耗 相對較大
靈活性 依賴looper,不靈活 Timer不依賴其他類

Timer執行的週期任務容易被自身干擾。(當耗時任務在sched()中執行時候,會大大延遲下一次任務的執行;當耗時任務需要操作同一個物件在scheduleAtFixedRate()中執行的時候,拿不到任務物件,等待上一次的任務釋放鎖。)

小結

Handler適合大多數場景,且好處理。 Timer只適合執行耗時比較少的重複任務。 難怪Timer相關文章熱度這麼低,看完原始碼才知道,是個小辣雞。這兩天時間算是浪費了。

Timer機制原始碼淺析

最後希望大家多多關注我們的部落格團隊:天星技術部落格https://juejin.im/user/5afa539751882542aa42e5c5

相關文章