更新時(shí)間:2022-10-19 11:18:55 來(lái)源:動(dòng)力節(jié)點(diǎn) 瀏覽2038次
Timer和TimerTask是我們用來(lái)在后臺(tái)線程中調(diào)度任務(wù)的 java util 類(lèi)。基本上,TimerTask是要執(zhí)行的任務(wù),Timer是調(diào)度程序。
(1)在給定的延遲之后
讓我們從在Timer的幫助下簡(jiǎn)單地運(yùn)行單個(gè)任務(wù)開(kāi)始:
@Test
public void givenUsingTimer_whenSchedulingTaskOnce_thenCorrect() {
TimerTask task = new TimerTask() {
public void run() {
System.out.println("Task performed on: " + new Date() + "n" +
"Thread's name: " + Thread.currentThread().getName());
}
};
Timer timer = new Timer("Timer");
long delay = 1000L;
timer.schedule(task, delay);
}
這會(huì)在一定的延遲后執(zhí)行任務(wù),我們將其作為schedule()方法的第二個(gè)參數(shù)。
請(qǐng)注意,如果我們將其作為 JUnit 測(cè)試運(yùn)行,我們應(yīng)該添加一個(gè)Thread.sleep(delay * 2)調(diào)用以允許 Timer 的線程在 Junit 測(cè)試停止執(zhí)行之前運(yùn)行任務(wù)。
(2)在給定的日期和時(shí)間
現(xiàn)在讓我們看一下Timer#schedule(TimerTask, Date)方法,它的第二個(gè)參數(shù)是Date而不是long 。這允許我們?cè)谀硞€(gè)時(shí)刻安排任務(wù),而不是延遲之后。
這一次,讓我們假設(shè)我們有一個(gè)舊的遺留數(shù)據(jù)庫(kù),我們希望將其數(shù)據(jù)遷移到具有更好模式的新數(shù)據(jù)庫(kù)中。
我們可以創(chuàng)建一個(gè)DatabaseMigrationTask類(lèi)來(lái)處理這個(gè)遷移:
public class DatabaseMigrationTask extends TimerTask {
private List<String> oldDatabase;
private List<String> newDatabase;
public DatabaseMigrationTask(List<String> oldDatabase, List<String> newDatabase) {
this.oldDatabase = oldDatabase;
this.newDatabase = newDatabase;
}
@Override
public void run() {
newDatabase.addAll(oldDatabase);
}
}
為簡(jiǎn)單起見(jiàn),我們用String的List來(lái) 表示這兩個(gè)數(shù)據(jù)庫(kù)。簡(jiǎn)而言之,我們的遷移包括將第一個(gè)列表中的數(shù)據(jù)放入第二個(gè)列表中。
要在所需的時(shí)刻執(zhí)行此遷移,我們必須使用 schedule ()方法的重載版本:
List<String> oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill");
List<String> newDatabase = new ArrayList<>();
LocalDateTime twoSecondsLater = LocalDateTime.now().plusSeconds(2);
Date twoSecondsLaterAsDate = Date.from(twoSecondsLater.atZone(ZoneId.systemDefault()).toInstant());
new Timer().schedule(new DatabaseMigrationTask(oldDatabase, newDatabase), twoSecondsLaterAsDate);
如我們所見(jiàn),我們將遷移任務(wù)以及執(zhí)行日期賦予schedule()方法。
然后在twoSecondsLater指示的時(shí)間執(zhí)行遷移:
while (LocalDateTime.now().isBefore(twoSecondsLater)) {
assertThat(newDatabase).isEmpty();
Thread.sleep(500);
}
assertThat(newDatabase).containsExactlyElementsOf(oldDatabase);
在此之前,不會(huì)發(fā)生遷移。
既然我們已經(jīng)介紹了如何安排任務(wù)的單次執(zhí)行,那么讓我們看看如何處理可重復(fù)的任務(wù)。
再一次,Timer類(lèi)提供了多種可能性。我們可以設(shè)置重復(fù)以觀察固定延遲或固定速率。
固定延遲意味著執(zhí)行將在最后一次執(zhí)行開(kāi)始后的一段時(shí)間內(nèi)開(kāi)始,即使它被延遲(因此本身被延遲)。
假設(shè)我們希望每?jī)擅氚才乓淮稳蝿?wù),第一次執(zhí)行需要一秒鐘,第二次執(zhí)行需要兩秒鐘,但會(huì)延遲一秒鐘。然后第三次執(zhí)行從第五秒開(kāi)始:
0s 1s 2s 3s 5s
|--T1--|
|-----2s-----|--1s--|-----T2-----|
|-----2s-----|--1s--|-----2s-----|--T3--|
另一方面,固定速率意味著每次執(zhí)行都將遵循初始計(jì)劃,無(wú)論之前的執(zhí)行是否已延遲。
讓我們重用之前的示例。在固定速率下,第二個(gè)任務(wù)將在三秒后開(kāi)始(由于延遲),但第三個(gè)任務(wù)將在四秒后開(kāi)始(尊重每?jī)擅雸?zhí)行一次的初始計(jì)劃):
0s 1s 2s 3s 4s
|--T1--|
|-----2s-----|--1s--|-----T2-----|
|-----2s-----|-----2s-----|--T3--|
現(xiàn)在我們已經(jīng)介紹了這兩個(gè)原則,讓我們看看如何使用它們。
為了使用固定延遲調(diào)度,還有兩個(gè)schedule()方法的重載,每個(gè)都采用一個(gè)額外的參數(shù)來(lái)說(shuō)明以毫秒為單位的周期。
為什么要重載兩次?因?yàn)槿匀挥锌赡茉谀硞€(gè)時(shí)刻或某個(gè)延遲后開(kāi)始任務(wù)。
至于固定速率調(diào)度,我們有兩個(gè) scheduleAtFixedRate()方法,它們也以毫秒為單位。同樣,我們有一種方法可以在給定的日期和時(shí)間啟動(dòng)任務(wù),另一種方法可以在給定的延遲后啟動(dòng)它。
同樣值得一提的是,如果一個(gè)任務(wù)需要更多的時(shí)間來(lái)執(zhí)行,它延遲整個(gè)鏈的執(zhí)行,我們是否使用固定的延遲或固定利率。
(1)與一個(gè)固定的延遲
現(xiàn)在讓我們想象我們要實(shí)現(xiàn)一個(gè)通訊系統(tǒng),每周發(fā)送電子郵件到我們的追隨者。在這種情況下,重復(fù)性任務(wù)似乎是理想的。
我們計(jì)劃每秒鐘通訊,基本上是垃圾郵件,但是發(fā)送是假的,我們好了。
首先,我們將設(shè)計(jì)一個(gè)NewsletterTask:
public class NewsletterTask extends TimerTask {
@Override
public void run() {
System.out.println("Email sent at: "
+ LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()),
ZoneId.systemDefault()));
}
}
在每次執(zhí)行時(shí),任務(wù)將打印它的預(yù)定時(shí)間,我們收集使用TimerTask # scheduledExecutionTime()方法。
那么如果我們想在固定延遲模式下每秒安排一次這個(gè)任務(wù)呢?我們必須使用前面提到的schedule()的重載版本:
new Timer().schedule(new NewsletterTask(), 0, 1000);
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
}
當(dāng)然,我們只對(duì)少數(shù)情況進(jìn)行測(cè)試:
Email sent at: 2020-01-01T10:50:30.860
Email sent at: 2020-01-01T10:50:31.860
Email sent at: 2020-01-01T10:50:32.861
Email sent at: 2020-01-01T10:50:33.861
正如我們所見(jiàn),每次執(zhí)行之間至少間隔一秒,但有時(shí)會(huì)延遲一毫秒。這種現(xiàn)象是由于我們決定使用固定延遲重復(fù)。
(2)固定利率
現(xiàn)在,如果我們要使用固定速率重復(fù)呢?然后我們將不得不使用scheduleAtFixedRate()方法:
new Timer().scheduleAtFixedRate(new NewsletterTask(), 0, 1000);
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
}
這一次,執(zhí)行不會(huì)被之前的延遲:
Email sent at: 2020-01-01T10:55:03.805
Email sent at: 2020-01-01T10:55:04.805
Email sent at: 2020-01-01T10:55:05.805
Email sent at: 2020-01-01T10:55:06.805
(3)安排每日任務(wù)
接下來(lái),讓我們每天運(yùn)行一次任務(wù):
@Test
public void givenUsingTimer_whenSchedulingDailyTask_thenCorrect() {
TimerTask repeatedTask = new TimerTask() {
public void run() {
System.out.println("Task performed on " + new Date());
}
};
Timer timer = new Timer("Timer");
long delay = 1000L;
long period = 1000L * 60L * 60L * 24L;
timer.scheduleAtFixedRate(repeatedTask, delay, period);
}
可以通過(guò)幾種方式取消任務(wù)的執(zhí)行。
(1)在Run中取消TimerTask
第一個(gè)選項(xiàng)是在TimerTask本身的run()方法實(shí)現(xiàn)中調(diào)用TimerTask.cancel()方法:
@Test
public void givenUsingTimer_whenCancelingTimerTask_thenCorrect()
throws InterruptedException {
TimerTask task = new TimerTask() {
public void run() {
System.out.println("Task performed on " + new Date());
cancel();
}
};
Timer timer = new Timer("Timer");
timer.scheduleAtFixedRate(task, 1000L, 1000L);
Thread.sleep(1000L * 2);
}
(2)取消定時(shí)器
另一種選擇是在Timer對(duì)象上調(diào)用Timer.cancel()方法:
@Test
public void givenUsingTimer_whenCancelingTimer_thenCorrect()
throws InterruptedException {
TimerTask task = new TimerTask() {
public void run() {
System.out.println("Task performed on " + new Date());
}
};
Timer timer = new Timer("Timer");
timer.scheduleAtFixedRate(task, 1000L, 1000L);
Thread.sleep(1000L * 2);
timer.cancel();
}
(3)在Run中停止TimerTask的線程
我們也可以在任務(wù)的run方法中停止線程,從而取消整個(gè)任務(wù):
@Test
public void givenUsingTimer_whenStoppingThread_thenTimerTaskIsCancelled()
throws InterruptedException {
TimerTask task = new TimerTask() {
public void run() {
System.out.println("Task performed on " + new Date());
// TODO: stop the thread here
}
};
Timer timer = new Timer("Timer");
timer.scheduleAtFixedRate(task, 1000L, 1000L);
Thread.sleep(1000L * 2);
}
注意run實(shí)現(xiàn)中的 TODO 指令;為了運(yùn)行這個(gè)簡(jiǎn)單的例子,我們需要真正停止線程。
在現(xiàn)實(shí)世界的自定義線程實(shí)現(xiàn)中,應(yīng)該支持停止線程,但在這種情況下,我們可以忽略棄用并在 Thread 類(lèi)本身上使用簡(jiǎn)單的停止API。
我們還可以很好地利用 ExecutorService 來(lái)安排定時(shí)器任務(wù),而不是使用定時(shí)器。
下面是一個(gè)快速示例,說(shuō)明如何以指定的時(shí)間間隔運(yùn)行重復(fù)任務(wù):
@Test
public void givenUsingExecutorService_whenSchedulingRepeatedTask_thenCorrect()
throws InterruptedException {
TimerTask repeatedTask = new TimerTask() {
public void run() {
System.out.println("Task performed on " + new Date());
}
};
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
long delay = 1000L;
long period = 1000L;
executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS);
Thread.sleep(delay + period * 3);
executor.shutdown();
}
那么Timer和ExecutorService解決方案的主要區(qū)別是什么:
定時(shí)器可以對(duì)系統(tǒng)時(shí)鐘的變化敏感;ScheduledThreadPoolExecutor不是。
Timer只有一個(gè)執(zhí)行線程;ScheduledThreadPoolExecutor可以配置任意數(shù)量的線程。
TimerTask中拋出的運(yùn)行時(shí)異常會(huì)殺死線程,因此以下計(jì)劃任務(wù)不會(huì)進(jìn)一步運(yùn)行;使用ScheduledThreadExecutor,當(dāng)前任務(wù)將被取消,但其余任務(wù)將繼續(xù)運(yùn)行。
相關(guān)閱讀
0基礎(chǔ) 0學(xué)費(fèi) 15天面授
有基礎(chǔ) 直達(dá)就業(yè)
業(yè)余時(shí)間 高薪轉(zhuǎn)行
工作1~3年,加薪神器
工作3~5年,晉升架構(gòu)
提交申請(qǐng)后,顧問(wèn)老師會(huì)電話與您溝通安排學(xué)習(xí)
初級(jí) 202925
初級(jí) 203221
初級(jí) 202629
初級(jí) 203743