Java定時任務調度(1)TimerTask原理與實戰

這是我參與新手入門的第1篇文章。java

1.0 簡單的例子

public static void main(String[] args) {
	TimerTask timerTask = new TimerTask() {
		@Override
		public void run() {
			while (true) {
		        try {
		            Thread.sleep(2000);
		        } catch (Exception e) {
		            // TODO: handle exception
		        }
		        System.out.println("TimerTask當前時間:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
			}
		}
	};
	Timer timer = new Timer();
	long delay = 0;
	long period = 1000;
	timer.schedule(timerTask, delay, period);
}
複製代碼

效果:web

image.png

TimerTask的原理是什麼那?api

實際上經過上方的示例代碼你就能發現,TimerTask就是一個實現了run方法的類,TimerTask是一個抽象類,實現了Runnable,提供了抽象方法run()。數組

public abstract class TimerTask implements Runnable {
    //task的狀態,默認是VIRGIN,共有4個狀態
    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;
    
	/** * The action to be performed by this timer task. */
    public abstract void run();
    
    //這個就是把當前這個task設置爲取消狀態,Timer也有一個cancel方法,會面會提到
    public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }
    
    //還有一個scheduledExecutionTime方法 就不提了
	
}
複製代碼

關鍵在於 Timer 💥markdown

image.png

1.1 Timer的構造函數

  1. 先來看一下第三個構造方法,方法體就是傳入並設置線程的名字,而後啓動。
public Timer(String name) {
     thread.setName(name);
     thread.start();
 }
複製代碼

這裏的thread就是一個線程,數據結構

/** * The timer thread. */
     private final TimerThread thread = new TimerThread(queue);
複製代碼

而這個TimerThread就被定義在Timer類中,從註釋中能夠看出,這個線程就是用來執行定時任務的具體線程,當隊列中的任務被觸發時執行它們。app

我會在1.3節中講這個TimerThread和隊列TaskQueue。😎ide

/** * This "helper class" implements the timer's task execution thread, which * waits for tasks on the timer queue, executions them when they fire, * reschedules repeating tasks, and removes cancelled tasks and spent * non-repeating tasks from the queue. */
 class TimerThread extends Thread {
 ​
 }
複製代碼

此時,回過頭來再看其餘構造函數就一目瞭然了,第一個無參構造函數經過Timer爲前綴名構造一個線程,裏面的this就是上面這個構造函數,這裏的serialNumber()就是去生成一個名字。函數

public Timer() {
         this("Timer-" + serialNumber());
     }
 ​
     /** * This ID is used to generate thread names. */
     private final static AtomicInteger nextSerialNumber = new AtomicInteger(0);
     private static int serialNumber() {
         return nextSerialNumber.getAndIncrement();
     }
複製代碼

若是有不明白AtomicInteger()的朋友能夠看着這篇博客: blog.csdn.net/fanrenxiang…oop

在代碼裏打個斷點就能夠清楚的看到這個線程的名字:

image.png

  1. 第二個構造函數傳入了是否爲後臺線程,若是是,主線程結束後會自動結束,不須要調用cancel。

public Timer(boolean isDaemon) {
         this("Timer-" + serialNumber(), isDaemon);
     }
複製代碼

爲何要設置爲後臺線程,很經典的一個例子是在web應用中使用線程,當你把web應用關閉後,這個線程還在運行!🐄🍺

這是由於線程是JVM級別的,web應用關閉後,這個線程並無銷燬。具體能夠看這篇博客:blog.csdn.net/chetianyao8…

  1. 第四個構造函數能夠設置名字及是否爲後臺線程,而且啓動。

public Timer(String name, boolean isDaemon) {
         thread.setName(name);
         thread.setDaemon(isDaemon);
         thread.start();
     }
複製代碼

好,下面開始講Timer的方法,屬性放到1.3節中

1.2 Timer的方法

timer提供了6個調度方法,其實都大同小異,先了解一下,看完1.3節再回過來看

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);
     }
複製代碼

能夠發現,最終都調用了sched方法,而schedulescheduleAtFixedRate的區別就是傳入的最終period的正負數,爲何會有這種區別?看完後面就懂了。

sched方法就是對task作一些設置,加到隊列裏,並作一次notify操做。這些後面都會提到。

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;   //task狀態
             }
             //添加到隊列
             queue.add(task);
             if (queue.getMin() == task)
                 queue.notify();
         }
     }
複製代碼

cancel()方法,一旦執行,Timer就停掉了,爲何要把newTasksMayBeScheduled設置爲false以及爲什調用notify(),也須要看後面的講解。

public void cancel() {
         synchronized(queue) {
             thread.newTasksMayBeScheduled = false;
             queue.clear();
             queue.notify();  // In case queue was already empty.
         }
     }
複製代碼

purge()方法,當對TimerTask作了屢次cancel以後,隊列就混亂了,這時候就須要調用這個方法,回收空間並從新排列。

注意,這個cancel不是上面講Timer的cancel方法,是TimerTask的cancel方法

public int purge() {
          int re0sult = 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;
      }
複製代碼

1.3 Timer的屬性

在上面已經提到過了Timer的兩個屬性thread和nextSerialNumber,此外還有一個queue,這個queue存儲的就是要調度的任務,也就是TimerTask。

1.3.1 TaskQueue

TaskQueue的結構很簡單,一個數組,一個size。須要注意的一點是此隊列存儲範圍是queue[1]-queue[size]。隊列默認大小是128,但,是支持擴容的。

private final TaskQueue queue = new TaskQueue();
     
     class TaskQueue {
         private TimerTask[] queue = new TimerTask[128];
         private int size = 0;
     }
複製代碼

擴容的方式是在增長TimerTask時進行操做

void add(TimerTask task) {
         // 擴容在這
         if (size + 1 == queue.length)
             queue = Arrays.copyOf(queue, 2*queue.length);
 ​
         queue[++size] = task;
         fixUp(size);
     }
複製代碼

TaskQueue提供了一系列的操做來對隊列進行處理,尤爲是排序。這裏面涉及很多數據結構的操做,大學數據結構不及格的能夠學一波。

TaskQueue原理上是一個平衡二叉堆

image.png

根節點的nextExecutionTime最小,queue[n]的子節點是queue[2n]和queue[2n+1],這些亂七八糟的就不說了,課本上都有。

size()、get(i)和add(TimerTask)這三個方法就不說了,getMin()是返回最近須要執行的任務,返回的就是queue[1]。

removeMin()是刪除當前最近執行的任務,而刪除的操做很經典,大學數據結構必學的點,就是把隊尾賦給對頭,隊尾置爲null,而且從新排序。

void removeMin() {
         queue[1] = queue[size];
         queue[size--] = null;  // Drop extra reference to prevent memory leak
         fixDown(1);
     }
複製代碼

fixDown(int)的做用就是下濾,不斷的把queue[k]和它的子節點進行比較,直到它的nextExecutionTime小於等於子節點。

private void fixDown(int k) {
         int j;
         while ((j = k << 1) <= size && j > 0) {
             if (j < size &&
                 queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
                 j++; // j indexes smallest kid
             if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
                 break;
             TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
             k = j;
         }
     }
複製代碼

fixUp(int)的做用是上溢,不斷向上提高,在新增TimerTask時使用。

private void fixUp(int k) {
         while (k > 1) {
             int j = k >> 1;
             if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                 break;
             TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
             k = j;
         }
     }
複製代碼

quickRemove(int) 刪除指定的元素。爲何叫quick那,它只賦值沒有排序,排序要搭配heapify()。

void quickRemove(int i) {
         assert i <= size;
 ​
         queue[i] = queue[size];
         queue[size--] = null;  // Drop extra ref to prevent memory leak
     }
     
     void heapify() {
         //對queue的後半段作排序
         for (int i = size/2; i >= 1; i--)
             fixDown(i);
     }
複製代碼

rescheduleMin(newTime)從新設置當前任務的下一次執行時間並排序,爲何取queue[1]?

由於在真正執行的時候,是每次取下次執行時間最短的,也就是queue[1]。

void rescheduleMin(long newTime) {
         queue[1].nextExecutionTime = newTime;
         fixDown(1);
     }
複製代碼

1.3.2 TimerThread

在1.1節簡單的提了下TimerThread,這裏重點介紹下,由於它是Timer的重點👴👴👴

TimerThread很簡單,TimerThread就是具體去執行的地方。

先來看下TimerThread的源碼,主體就是 mainLoop() 方法。

class TimerThread extends Thread {
     //標誌位,用於在mainLoop中判斷狀態
     boolean newTasksMayBeScheduled = true;
     
     private TaskQueue queue;
     
     TimerThread(TaskQueue queue) {
         this.queue = queue;
     }
     
     public void run() {
         try {
             //mainLoop的具體實現,下面會講
             mainLoop();
         } finally {
             // 將參數置爲false,而且隊列清空
             synchronized(queue) {
                 newTasksMayBeScheduled = false;
                 queue.clear();  // Eliminate obsolete references
             }
         }
     }
 }
複製代碼

那麼TimerThread是如何運行的?我來給你捋一下

//一開始咱們是new了一個Timer(),經過Timer的無參構造函數配置了默認的線程名字並執行了thread.start()
 //而且經過Timer的schedule()方法把TimerTask放到隊列裏並設置了延遲、週期和狀態。
 Timer timer = new Timer();
 timer.schedule(timerTask, delay, period);
 ​
 public Timer() {
     this("Timer-" + serialNumber());
 }
 ​
 public Timer(String name) {
     //這裏的thread就是被final修飾的TimerThread,new了一個TimerThread並傳遞了隊列queue
     //private final TimerThread thread = new TimerThread(queue);
     
     thread.setName(name);
     thread.start();
 }
 ​
 //而TimerThread是有一個TaskQueue的屬性和重載的構造函數,這個重載的構造函數接收了queue
 private TaskQueue queue;
 ​
 TimerThread(TaskQueue queue) {
     this.queue = queue;
 }
 ​
 //一旦執行thread.start(),TimerThread的run方法就會執行,具體執行就在mainLoop()方法中
 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) {
         }
     }
 }
 ​
     //大致讀一下這個方法,你就會發現除非遇到break或者遇到不能捕獲的異常,它就是個死循環。while (queue.isEmpty() && newTasksMayBeScheduled)
                         queue.wait();
 ​
     //跳出循環的條件就是queue不爲空或者newTasksMayBeScheduled爲false
     //那麼wait就是等到其餘地方對queue發生nitify操做,其實就是在調用cancel的時候
     //這時標誌位是false,會跳出循環,而且對queue設置了clear操做, 直接就跳出了外部的死循環。public void cancel() {
         synchronized(queue) {
             thread.newTasksMayBeScheduled = false;
             queue.clear();
             queue.notify();  // In case queue was already empty.
         }
     }
 ​
     //還有一處,是Timer的屬性threadReaper調用finalize的時候
     //這個threadReaper只重寫了finalize方法,GC的時候調用private final Object threadReaper = new Object() {
         protected void finalize() throws Throwable {
             synchronized(queue) {
                 thread.newTasksMayBeScheduled = false;
                 queue.notify(); // In case queue is empty.
             }
         }
     };
     //還有就是當對queue執行add操做的時候,在Timer中的sched方法,前邊有提到。此時queue不爲空,就跳出了循環。//以後是判斷該task是否被取消
     if (task.state == TimerTask.CANCELLED) {
         queue.removeMin();
         continue;  // No action required, poll queue again
     }
 ​
     //再以後是取當前的系統時間和上次預計的執行時間,若是當前系統時間已經超了,就趕忙執行。
     //不過在執行以前須要判斷是不是重複任務。
     //判斷一下period是否爲0,0就表明一次性任務,刪掉。若是不是,就調用rescheduleMin設置下一次執行時間並排序。
 ​
     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);
         }
     }
 ​
     //這裏頗有意思,若是period是負數,下次執行時間就是當前系統時間+週期時間。若是是正數就是原計算的下次執行時間+週期時間。
     //這就是schedule和scheduleAtFixedRate的區別。換一下參數的正負數,就和另外一個方法同樣。。//再日後就是若是當前這個task執行時間還沒到就等待一段時間,
     if (!taskFired) // Task hasn't yet fired; wait
         queue.wait(executionTime - currentTime);
 ​
     //若是時間到了,就執行了
     if (taskFired)  // Task fired; run it, holding no locks
         task.run();
 ​
複製代碼

好,至此整個執行流程就結束了,是否是很簡單!😏😏😏

在回過頭去看1.2節,是否是清晰又明瞭!👻

1.4 Timer總結

Timer能夠分四個部分:

  • TimerTask是調度任務的具體內容
  • TaskQueue存放要執行的TimerTask,下標越小優先級最高。
  • TimerThread是Thread的擴展類,從TaskQueue中獲取下標爲1 的TimerTask執行,並根據是不是重複任務對TaskQueue進行處理。
  • Timer主要就是配置任務執行時間、間隔、執行內容,TimerThread和TaskQueue位於Timer類中。

須要注意的點是,若是要用TimerTask,必定要記得使用try catch,若是遇到不能捕獲的異常Timer就終止了。

這裏能夠參考一個生產過程當中遇到的問題:blog.verysu.com/article/435

相關文章
相關標籤/搜索