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源碼的學習,能更進一步的了解線程和隊列方面的知識。在實際的項目中,出于性能和資源的考慮,我們可能不會這麼簡單粗暴的使用這種方式來實作我們需要的定時功能,原因在于,當我們需要定時執行的任務很多的時候,這種方式會生成很多的線程出來,這是很消耗資源的,同時線程的排程也是很占資源的。是以,後面的部落格會介紹其他的實作定時任務的方式,通過線程池的方式管理線程,提高線程使用率,進而提高效率。
歡迎一起學習交流。