您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關在Java應用程序中如何安排重復性任務的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。
java.util.Timer
和java.util.TimerTask
類(我將二者統稱為Java的定時器框架)使得程序員可以輕松地安排簡單的任務。(請注意,這些類在 J2ME 中也可用。)在 Java 2 SDK 標準版 1.3 版中引入此框架之前,開發人員必須編寫自己的調度程序,這涉及處理線程和Object.wait()
方法的復雜性。但是,Java 定時器框架不夠豐富,無法滿足許多應用程序的調度需求。即使是需要每天同時重復的任務也不能直接使用Timer
進行調度,因為夏令時的來來往往會發生時間跳躍。
調度框架構建在 Java 計時器框架類之上。因此,在解釋調度框架的使用方式和實現方式之前,我們將首先了解如何使用這些類進行調度。
想象一個雞蛋計時器,它通過播放聲音告訴您何時過去了數分鐘(因此你的雞蛋已煮熟)。清單 1 中的代碼構成了用 Java 語言編寫的簡單的雞蛋計時器的基礎:
package org.tiling.scheduling.examples;
import java.util.Timer;
import java.util.TimerTask;
public class EggTimer {
private final Timer timer = new Timer();
private final int minutes;
public EggTimer(int minutes) {
this.minutes = minutes;
}
public void start() {
timer.schedule(new TimerTask() {
public void run() {
playSound();
timer.cancel();
}
private void playSound() {
System.out.println("Your egg is ready!");
// Start a new thread to play a sound...
}
}, minutes ? 60 ? 1000);
}
public static void main(String[] args) {
EggTimer eggTimer = new EggTimer(2);
eggTimer.start();
}
}
一個EggTimer
實例擁有一個Timer
實例來提供必要的調度。當使用start()
方法啟動雞蛋計時器時,它會安排 aTimerTask
在指定的分鐘數后執行。當時間到時, 上的run()
方法TimerTask
由Timer
幕后調用,使其播放聲音。然后應用程序在定時器被取消后終止。
Timer允許通過指定固定的執行速率或執行之間的固定延遲來安排任務重復執行。但是,有許多應用程序具有更復雜的調度要求。例如,每天早上在同一時間響起叫醒電話的鬧鐘不能簡單地使用 86400000 毫秒(24 小時)的固定速率時間表,因為在時鐘前進的日子里,鬧鐘會太晚或太早或向后(如果您的時區使用夏令時)。解決方案是使用日歷算法來計算每日事件的下一次預定發生。這正是調度框架所支持的。考慮AlarmClock 清單 2 中的實現(請參閱相關鏈接以下載調度框架的源代碼,以及包含框架和示例的 JAR 文件):
package org.tiling.scheduling.examples;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.tiling.scheduling.Scheduler;
import org.tiling.scheduling.SchedulerTask;
import org.tiling.scheduling.examples.iterators.DailyIterator;
public class AlarmClock {
private final Scheduler scheduler = new Scheduler();
private final SimpleDateFormat dateFormat =
new SimpleDateFormat("dd MMM yyyy HH:mm:ss.SSS");
private final int hourOfDay, minute, second;
public AlarmClock(int hourOfDay, int minute, int second) {
this.hourOfDay = hourOfDay;
this.minute = minute;
this.second = second;
}
public void start() {
scheduler.schedule(new SchedulerTask() {
public void run() {
soundAlarm();
}
private void soundAlarm() {
System.out.println("Wake up! " +
"It's " + dateFormat.format(new Date()));
// Start a new thread to sound an alarm...
}
}, new DailyIterator(hourOfDay, minute, second));
}
public static void main(String[] args) {
AlarmClock alarmClock = new AlarmClock(7, 0, 0);
alarmClock.start();
}
}
請注意代碼與雞蛋計時器應用程序的相似程度。AlarmClock實例擁有Scheduler實例(而不是一個Timer)提供必要的調度。啟動時,鬧鐘會安排 a SchedulerTask(而不是 a TimerTask)來播放鬧鐘。而不是在固定延遲后安排任務執行,鬧鐘使用一個DailyIterator類來描述它的時間表。在這種情況下,它只是在每天早上 7:00 安排任務。這是典型運行的輸出:
Wake up! It's 24 Aug 2003 07:00:00.023
Wake up! It's 25 Aug 2003 07:00:00.001
Wake up! It's 26 Aug 2003 07:00:00.058
Wake up! It's 27 Aug 2003 07:00:00.015
Wake up! It's 28 Aug 2003 07:00:00.002
...
DailyIterator
實現ScheduleIterator
接口,該接口將SchedulerTask
的計劃執行時間指定為一系列java.util.Date
對象。然后,next()
方法按時間順序迭代對象。返回值null
會導致任務被取消(也就是說,它永遠不會再次運行)——實際上,重新調度的嘗試將導致拋出異常。清單 3 包含ScheduleIterator接口:
package org.tiling.scheduling;
import java.util.Date;
public interface ScheduleIterator {
public Date next();
}
DailyIterator的next()
方法返回Date表示每天同一時間(上午 7:00)的對象,如清單 4 所示。因此,如果你調用next()
一個新構造的DailyIterator類,您將獲得該日期當天或之后的當天上午 7:00傳入構造函數。隨后的調用next()
將在隨后幾天的上午 7:00 返回,并永遠重復。要實現此行為,請DailyIterator使用java.util.Calendar實例。構造函數設置日歷,以便第一次調用next()返回正確的,Date只需在日歷上添加一天。請注意,該代碼沒有明確提及夏令時修正;它不需要,因為Calendar實現(在這種情況下GregorianCalendar)會處理這個問題。
package org.tiling.scheduling.examples.iterators; import org.tiling.scheduling.ScheduleIterator; import java.util.Calendar; import java.util.Date; /?? ? A DailyIterator class returns a sequence of dates on subsequent days ? representing the same time each day. ?/ public class DailyIterator implements ScheduleIterator { private final int hourOfDay, minute, second; private final Calendar calendar = Calendar.getInstance(); public DailyIterator(int hourOfDay, int minute, int second) { this(hourOfDay, minute, second, new Date()); } public DailyIterator(int hourOfDay, int minute, int second, Date date) { this.hourOfDay = hourOfDay; this.minute = minute; this.second = second; calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, second); calendar.set(Calendar.MILLISECOND, 0); if (!calendar.getTime().before(date)) { calendar.add(Calendar.DATE, ?1); } } public Date next() { calendar.add(Calendar.DATE, 1); return calendar.getTime(); } }
在上一節中,我們學習了如何使用調度框架,并將其與 Java 定時器框架進行了比較。接下來,我將向你展示該框架是如何實現的。除了ScheduleIterator
在顯示界面清單3中,還有另外兩個類-Scheduler
和SchedulerTask
-組成的框架。這些類實際上在封面下使用Timer
和TimerTask
,因為日程實際上只不過是一個系列的一次性計時器。清單 5 和
6 顯示了這兩個類的源代碼:
package org.tiling.scheduling;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
public class Scheduler {
class SchedulerTimerTask extends TimerTask {
private SchedulerTask schedulerTask;
private ScheduleIterator iterator;
public SchedulerTimerTask(SchedulerTask schedulerTask,
ScheduleIterator iterator) {
this.schedulerTask = schedulerTask;
this.iterator = iterator;
}
public void run() {
schedulerTask.run();
reschedule(schedulerTask, iterator);
}
}
private final Timer timer = new Timer();
public Scheduler() {
}
public void cancel() {
timer.cancel();
}
public void schedule(SchedulerTask schedulerTask,
ScheduleIterator iterator) {
Date time = iterator.next();
if (time == null) {
schedulerTask.cancel();
} else {
synchronized(schedulerTask.lock) {
if (schedulerTask.state != SchedulerTask.VIRGIN) {
throw new IllegalStateException("Task already
scheduled " + "or cancelled");
}
schedulerTask.state = SchedulerTask.SCHEDULED;
schedulerTask.timerTask =
new SchedulerTimerTask(schedulerTask, iterator);
timer.schedule(schedulerTask.timerTask, time);
}
}
}
private void reschedule(SchedulerTask schedulerTask,
ScheduleIterator iterator) {
Date time = iterator.next();
if (time == null) {
schedulerTask.cancel();
} else {
synchronized(schedulerTask.lock) {
if (schedulerTask.state != SchedulerTask.CANCELLED) {
schedulerTask.timerTask =
new SchedulerTimerTask(schedulerTask, iterator);
timer.schedule(schedulerTask.timerTask, time);
}
}
}
}
}
清單 6 顯示了SchedulerTask該類的源代碼:
package org.tiling.scheduling; import java.util.TimerTask; public abstract class SchedulerTask implements Runnable { final Object lock = new Object(); int state = VIRGIN; static final int VIRGIN = 0; static final int SCHEDULED = 1; static final int CANCELLED = 2; TimerTask timerTask; protected SchedulerTask() { } public abstract void run(); public boolean cancel() { synchronized(lock) { if (timerTask != null) { timerTask.cancel(); } boolean result = (state == SCHEDULED); state = CANCELLED; return result; } } public long scheduledExecutionTime() { synchronized(lock) { return timerTask == null ? 0 : timerTask.scheduledExecutionTime(); } } }
就像雞蛋定時器一樣,調度器的每個實例都擁有一個計時器的實例,以提供底層調度。與用于實現雞蛋計時器的單一一次性計時器不同,調度器將一次性計時器串連在一起,以ScheduleIterator
指定的時間執行SchedulerTask
類。
考慮調度器公共的schedule()
方法——這是調度的入口點,因為它是客戶端調用的方法。(唯一的其他公共方法cancel()
,在Canceling tasks
中介紹。)所述的第一次執行的時間SchedulerTask,通過調用ScheduleIterator
接口上的next()
方法。然后通過調用底層Timer類上的one-shot schedule()
方法啟動調度,一邊在此時執行。為一次性執行提供的TimerTask
對象是嵌套SchedulerTimerTask
類的一個實例,它打包了任務和迭代器。在分配的時間內,run()
方法在嵌套類上調用,它使用打包的任務和迭代器引用來重新安排任務的下一次執行。reschedule()
方法與schedule()
方法非常相似,不同之處在于它是私有的,并且對SchedulerTask
執行一組略有不同的狀態檢查。重新調度過程無限重復,為每次調度的執行構造一個新的嵌套類實例,直到任務或調度程序被取消(或 JVM 關閉)。
與對應的TimerTask
一樣,SchedulerTask
在其生命周期中經歷一系列狀態。創建時,它處于一種VIRGIN
狀態,這意味著它從未被調度過。一旦被調度,它就會轉移到一個SCHEDULED
狀態,如果任務被下面描述的方法之一取消,則之后會切換到到CANCELLED
狀態。管理正確的狀態轉換,例如確保非VIRGIN
任務不會被調度兩次,會增加Scheduler
和SchedulerTask
類的額外復雜性。每當執行可能改變任務狀態的操作時,代碼必須在任務的鎖定對象上同步。
取消計劃任務的方式有三種。第一種是調用SchedulerTask
上的cancel()
方法。這就像TimerTask
上調用cancel()
:該任務將永遠不會再次運行,盡管如果已經運行,它將一直運行到完成。cancel()方法
的返回值是是一個布爾值,用來指示在尚未被調用cancel()
的情況下是否會運行進一步的計劃任務。更準確地說,如果任務在調用cancel()
之前立即處于SCHEDULED
狀態,它就會返回true
。如果你嘗試重新安排已取消(甚至已安排)的任務,Scheduler
則會拋出IllegalStateException
.
取消計劃任務的第二種方法是ScheduleIterator
返回null
。這只是第一種方式的快捷方式,因為Scheduler
類調用SchedulerTask
類上的cancel()
。如果你希望迭代器(而不是任務)控制調度何時停止,則以這種方式取消任務很有用。
第三種方式是通過調用它的cancel()
方法來取消整體的Scheduler
。這將取消調度程序的所有任務,并使其處于不能再調度更多任務的狀態。
調度框架可以比作 UNIX cron工具,除了調度時間的規范是命令式控制而不是聲明式控制。例如,DailyIterator
類在AlarmClock
實現中使用與cron
作業具有相同的調度,由0 7 * * *
開始的crontab
條目指定。(這些字段分別指定分鐘、小時、月中的某一天、月份和星期幾。)
但是,調度框架比cron具有更強大的靈活性。 想象一個HeatingController
應用程序在早上打開熱水。我想指示它“在工作日的早上 8:00 和周末早上 9:00 打開熱水”。使用cron,我需要兩個crontab
條目(0 8 * * 1,2,3,4,5
和0 9 * * 6,7
)。通過使用 ScheduleIterator
,解決方案更加優雅,因為我可以使用組合定義單個迭代器。清單 7 顯示了一種方法:
int[] weekdays = new int[] {
Calendar.MONDAY,
Calendar.TUESDAY,
Calendar.WEDNESDAY,
Calendar.THURSDAY,
Calendar.FRIDAY
};
int[] weekend = new int[] {
Calendar.SATURDAY,
Calendar.SUNDAY
};
ScheduleIterator i = new CompositeIterator(
new ScheduleIterator[] {
new RestrictedDailyIterator(8, 0, 0, weekdays),
new RestrictedDailyIterator(9, 0, 0, weekend)
}
);
一個RestrictedDailyIterator
類就像DailyIterator
,除了它被限制在一周的特定日期運行;并且一個CompositeIterator
類采用一組ScheduleIterators
并將日期正確地排序到一個單一的時間表中。
還有很多其他的調度cron不能產生,但是可以實現ScheduleIterator
。例如,“每個月的最后一天”描述的日程安排可以使用標準 Java 日歷算法(使用Calendar類)來實現,而使用cron. 應用程序甚至不必使用Calendar該類。在本文的源代碼中,我提供了一個安全燈控制器示例,該控制器按照“在日落前 15 分鐘開燈”的時間表運行。該實現使用 Calendrical Calculations 軟件包,計算本地日落時間(給定緯度和經度)。
在編寫使用調度的應用程序時,重要的是要了解框架在及時性方面的承諾。我的任務會提前還是推遲執行?如果是這樣,最大誤差幅度是多少?不幸的是,這些問題沒有簡單的答案。然而,在實踐中,該行為對于一大類應用程序來說已經足夠好了。下面的討論假設系統時鐘是正確的(有關網絡時間協議的信息,請參閱相關鏈接)。
因為Scheduler將其調度委托給Timer類,所以Scheduler可以做出的實時保證與Timer. Timer使用該Object.wait(long)方法調度任務。當前線程等待直到被喚醒,這可能是由于以下原因之一:
所述notify()
或notifyAll()
方法被稱為通過另一個線程的對象。
該線程被另一個線程中斷。
該線程在沒有通知的情況下被喚醒。(虛假喚醒)
指定的時間已經過去。
第一種可能性不會發生在Timer類上,因為wait()
被調用的對象是私有的。即便如此,Timer對前三個原因的提前喚醒實施了保障措施,從而確保線程在時間過去后喚醒。現在,文檔注釋Object.wait(long)
指出它可能會在“或多或少”時間過去后喚醒,因此線程可能會提前喚醒。在這種情況下,Timer發出另一個wait()
為(scheduledExecutionTime - System.currentTimeMillis())
毫秒,從而保證任務永遠不能被早期執行。
任務可以延遲執行嗎?是的。延遲執行的主要原因有兩個:線程調度和垃圾收集。
Java 語言規范在線程調度上故意含糊其辭。這是因為 Java 平臺是通用的,面向廣泛的硬件和相關操作系統。雖然大多數 JVM 實現都有一個公平的線程調度程序,但這并不能保證——當然,實現有不同的策略來為線程分配處理器時間。因此,當一個Timer線程在其分配的時間后喚醒時,它實際執行任務的時間取決于 JVM 的線程調度策略,以及有多少其他線程在爭用處理器時間。因此,為了減少延遲任務執行,您應該最大限度地減少應用程序中可運行線程的數量。值得考慮在單獨的 JVM 中運行調度程序來實現這一點。
JVM 執行垃圾收集 (GC) 所花費的時間對于創建大量對象的大型應用程序來說可能很重要。默認情況下,當 GC 發生時,整個應用程序必須等待它完成,這可能需要幾秒鐘或更長時間。(命令行選項-verbose:gc的java應用程序啟動器將導致每個 GC 事件都報告到控制臺。)為了盡量減少由于 GC 引起的暫停,這可能會阻礙即時任務執行,您應該盡量減少應用程序創建的對象數量。同樣,這有助于在單獨的 JVM 中運行您的調度代碼。此外,您可以嘗試使用多種優化選項來最小化 GC 暫停。例如,增量 GC 試圖將主要收集的成本分散到幾個次要收集上。代價是這會降低 GC 的效率,但對于更及時的調度來說,這可能是一個可以接受的代價。
為了確定任務是否正在及時運行,如果任務本身監視和記錄任何延遲執行的實例會有所幫助。SchedulerTask,如TimerTask,有一個scheduledExecutionTime()
方法返回最近一次執行此任務的時間。評估System.currentTimeMillis()
- scheduledExecutionTime()
任務開始時的表達式run()方法可讓您確定任務執行的延遲時間(以毫秒為單位)。可以記錄此值以生成有關延遲執行分布的統計信息。該值還可用于決定任務應該采取什么操作——例如,如果任務太晚,它可能什么都不做。如果在遵循上述指南后, 你的應用程序需要更嚴格的及時性保證。
感謝各位的閱讀!關于“在Java應用程序中如何安排重復性任務”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。