Java定時任務(一) Timer及TimerTask的案例解析及源碼分析
一、概述:
定時任務這個概念在Java的學習以及項目的開發中並不陌生,應用場景也是多種多樣。比如我們會注意到12306網站在鎖定車票的30分鐘以內要進行車票費用的支付,否則訂單會被取消;再比如我們的考試系統,考試管理人員上傳好考試信息,考試系統會根據考試信息準時開考;還有上課的鈴聲會在每週的上課時間準時響起,等等。這些生活細節只要我們稍微注意,留心觀察,便會發現編程已經早早地融入到我們生活的方方面面,定時任務也隨之應用到這些程序之中。因此,瞭解好定時任務,對我們的程序的編寫有很大的幫助。
二、使用案例:
在Java語言中實現定時任務這個功能,我們可以使用Java提供的Timer以及TimerTask來進行簡單的實現,jdk1.5之後也可以使用ScheduledExcecutorService 進行實現,後續的文章(定時任務(二))會介紹這種實現方式,並會在定時任務(三)(最後一篇)中介紹一個常見的應用場景中定時任務的實現案例。
這裏實現的案例是: 指定一個日期,並間隔一定的時間,週期性的執行一個任務,代碼如下:
public class TimerTaskTest {
public static void main(String[] args) throws ParseException {
//創建定時任務
TaskTest taskTest1 = new TaskTest("taskTest1");
//定時器創建
Timer timer = new Timer("定時器線程");
//指定開始執行的日期
Date startDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2019-03-03 10:55:10");
//時間間隔,單位是ms
long intervalTime = 2 * 1000;
try {
timer.schedule(taskTest1, startDate,intervalTime);
} catch (Exception e) {
e.printStackTrace();
}
}
}
class TaskTest extends TimerTask{
// 測試任務名稱
private String taskTestName;
public TaskTest(String taskTestName) {
super();
this.taskTestName = taskTestName;
}
@Override
public void run() {
//這裏是需要執行的具體任務
System.out.printf("定時任務 %s 執行中,響鈴響一下\n",taskTestName);
}
}
三、實現原理分析:
上面實現的定時任務,主要用到兩個類,一個是Timer類,一個是TimerTask類。Timer類主要用於維護定時任務,管理任務的執行,TimerTask是具體執行任務的類。Timer類的schedule方法可以設置任務的運行時機,在上面的案例中,運行方式爲:從startDate時間點開始,每隔intervalTime時間重複執行,執行的任務是taskTest1;TaskTest這個類繼承自TimerTask,並重寫了run方法,我們需要實際運行的任務代碼就是放在這裏。
Timer是定時器類,當實例化一個Timer對象的時候,這時新建了一個TaskQueue任務隊列並且新建了一個線程;任務隊列主要存放待執行的任務,線程主要負責管理待執行的任務。TimerThread線程需要以queue爲參數進行構造,如下圖:
TaskQueue是任務隊列,其實現方式是new了一個TimerTask數組,當調用schedule的時候,是向這個數組裏面添加元素,然後根據執行時間的先後對數組元素進行排序,從而確定最先開始執行的任務,如下圖:
開啓的TimerThread線程,會執行run方法裏面的mainLoop函數,這個函數會對TaskQueue隊列的首元素進行判斷,看是否達到執行時間,如果沒有,則進行休眠,休眠時間爲隊首任務的開始執行時間到當前時間的時間差。每當timer對象調用schedule方法時,都會向隊列添加元素,並喚醒TaskQueue隊列上的線程,這時候TimerThread會被喚醒,繼續執行mainLoop方法。如下圖所示:
mainLoop函數源代碼如下,函數執行的是一個死循環,並且加了queue鎖,從而保證是線程安全的。通過queue.getMin()找到任務隊列中執行時間最早的元素,然後判斷元素的state,period,nextExecutionTime,SCHEDULED等屬性,從而確定任務是否可執行。主要是判斷這幾個屬性:1,state 屬性,如果爲取消(即我們調用了timer的cancel方法取消了某一任務),則會從隊列中刪除這個元素,然後繼續循環;2,period 屬性,如果爲單次執行,這個值爲0,週期執行的話,爲我們傳入的intervalTime值,如果爲0,則會移出隊列,並設置任務狀態爲已執行,然後下面的 task.run()會執行任務,如果這個值不爲0,則會修正隊列,設置這個任務的再一次執行時間,queue.rescheduleMin這個函數來完成的這個操作;3,taskFired 屬性, 如果 executionTime<=currentTime 則設置爲true,可以執行,否則線程就會進行休眠,休眠時間爲兩者之差。
/**
* The main timer loop. (See class comment.)
*/
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) {
}
}
}
四、總結:
Java中的Timer和TimerTask可以實現基礎的定時任務, 他的實現主要用到了線程和隊列的組合,通過對timer源碼的學習,能更進一步的瞭解線程和隊列方面的知識。在實際的項目中,出於性能和資源的考慮,我們可能不會這麼簡單粗暴的使用這種方式來實現我們需要的定時功能,原因在於,當我們需要定時執行的任務很多的時候,這種方式會生成很多的線程出來,這是很消耗資源的,同時線程的調度也是很佔資源的。因此,後面的博客會介紹其他的實現定時任務的方式,通過線程池的方式管理線程,提高線程利用率,從而提高效率。
歡迎一起學習交流。