Java Timer

1. Timer - die Grundlagen

Timer und TimerTask sind Java-Util-Klassen, mit denen Aufgaben in einem Hintergrundthread geplant werden. In wenigen Worten - TimerTask ist die auszuführende Aufgabe und Timer ist der Scheduler .

2. Planen Sie eine Aufgabe einmal

2.1. Nach einer bestimmten Verzögerung

Beginnen wir damit, einfach eine einzelne Aufgabe mit Hilfe eines Timers auszuführen :

@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); }

Nun, dies führt die Aufgabe nach einer gewissen Verzögerung , die als zweiter Parameter der gegebenen schedule () Methode. Im nächsten Abschnitt erfahren Sie, wie Sie eine Aufgabe zu einem bestimmten Datum und zu einer bestimmten Uhrzeit planen.

Beachten Sie, dass wir, wenn dies ein JUnit-Test ist, einen Thread.sleep-Aufruf (delay * 2) hinzufügen sollten , damit der Timer-Thread die Aufgabe ausführen kann, bevor der Junit-Test nicht mehr ausgeführt wird.

2.2. Zu einem bestimmten Datum und einer bestimmten Uhrzeit

Schauen wir uns nun die Timer # Schedule- Methode (TimerTask, Date) an , bei der ein Datum anstelle eines Long als zweiter Parameter verwendet wird, sodass wir die Aufgabe zu einem bestimmten Zeitpunkt und nicht nach einer Verzögerung planen können.

Stellen wir uns diesmal vor, wir haben eine alte Legacy-Datenbank und möchten ihre Daten in eine neue Datenbank mit einem besseren Schema migrieren.

Wir könnten eine DatabaseMigrationTask- Klasse erstellen , die diese Migration handhabt:

public class DatabaseMigrationTask extends TimerTask { private List oldDatabase; private List newDatabase; public DatabaseMigrationTask(List oldDatabase, List newDatabase) { this.oldDatabase = oldDatabase; this.newDatabase = newDatabase; } @Override public void run() { newDatabase.addAll(oldDatabase); } }

Der Einfachheit halber stellen wir die beiden Datenbanken durch eine Liste von Zeichenfolgen dar . Einfach ausgedrückt besteht unsere Migration darin, die Daten aus der ersten Liste in die zweite Liste aufzunehmen.

Um diese Migration zum gewünschten Zeitpunkt durchzuführen, müssen wir die überladene Version der Schedule () -Methode verwenden :

List oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill"); List 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);

Wie wir sehen können, geben wir der Schedule () -Methode die Migrationsaufgabe sowie das Ausführungsdatum an .

Anschließend wird die Migration zu dem von twoSecondsLater angegebenen Zeitpunkt ausgeführt :

while (LocalDateTime.now().isBefore(twoSecondsLater)) { assertThat(newDatabase).isEmpty(); Thread.sleep(500); } assertThat(newDatabase).containsExactlyElementsOf(oldDatabase);

Während wir vor diesem Moment sind, findet die Migration nicht statt.

3. Planen Sie eine wiederholbare Aufgabe

Nachdem wir uns nun mit dem Planen der einzelnen Ausführung einer Aufgabe befasst haben, wollen wir sehen, wie mit wiederholbaren Aufgaben umgegangen wird.

Auch hier bietet die Timer- Klasse mehrere Möglichkeiten : Wir können die Wiederholung so einstellen, dass entweder eine feste Verzögerung oder eine feste Rate eingehalten wird.

Eine feste Verzögerung bedeutet, dass die Ausführung einen Zeitraum nach dem Zeitpunkt beginnt, zu dem die letzte Ausführung begonnen hat, auch wenn sie verzögert wurde (daher selbst verzögert wird) .

Angenommen, wir möchten alle zwei Sekunden eine Aufgabe planen, und die erste Ausführung dauert eine Sekunde und die zweite zwei, wird jedoch um eine Sekunde verzögert. Dann würde die dritte Ausführung mit der fünften Sekunde beginnen:

0s 1s 2s 3s 5s |--T1--| |-----2s-----|--1s--|-----T2-----| |-----2s-----|--1s--|-----2s-----|--T3--|

Andererseits bedeutet eine feste Rate, dass jede Ausführung den ursprünglichen Zeitplan einhält, unabhängig davon, ob sich eine vorherige Ausführung verzögert hat .

Lassen Sie uns unser vorheriges Beispiel mit einer festen Rate wiederverwenden. Die zweite Aufgabe startet nach drei Sekunden (aufgrund der Verzögerung). Aber der dritte nach vier Sekunden (unter Berücksichtigung des anfänglichen Zeitplans einer Ausführung alle zwei Sekunden):

0s 1s 2s 3s 4s |--T1--| |-----2s-----|--1s--|-----T2-----| |-----2s-----|-----2s-----|--T3--|

Da diese beiden Prinzipien behandelt werden, können wir sehen, wie sie verwendet werden.

Um die Zeitplanung mit fester Verzögerung zu verwenden, gibt es zwei weitere Überladungen der Methode sched () , die jeweils einen zusätzlichen Parameter enthalten, der die Periodizität in Millisekunden angibt.

Warum zwei Überladungen? Weil es immer noch die Möglichkeit gibt, die Aufgabe zu einem bestimmten Zeitpunkt oder nach einer bestimmten Verzögerung zu starten.

Für die Festpreisplanung haben wir die beiden ScheduleAtFixedRate () -Methoden, die ebenfalls eine Periodizität in Millisekunden annehmen. Wieder haben wir eine Methode, um die Aufgabe zu einem bestimmten Datum und einer bestimmten Uhrzeit zu starten, und eine andere, um sie nach einer bestimmten Verzögerung zu starten.

Es ist auch erwähnenswert, dass, wenn eine Aufgabe länger als die Ausführungszeit dauert, die gesamte Ausführungskette verzögert wird, unabhängig davon, ob wir eine feste Verzögerung oder eine feste Rate verwenden.

3.1. Mit einer festen Verzögerung

Stellen wir uns nun vor, wir möchten ein Newsletter-System implementieren und jede Woche eine E-Mail an unsere Follower senden. In diesem Fall erscheint eine sich wiederholende Aufgabe ideal.

Planen wir den Newsletter also jede Sekunde, was im Grunde genommen Spam ist, aber da das Senden falsch ist, können wir loslegen!

Lassen Sie uns zuerst eine Newsletter-Aufgabe entwerfen :

public class NewsletterTask extends TimerTask { @Override public void run() { System.out.println("Email sent at: " + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()), ZoneId.systemDefault())); } }

Bei jeder Ausführung druckt die Aufgabe ihre geplante Zeit, die wir mit der TimerTask # ScheduledExecutionTime () -Methode erfassen .

Was ist dann, wenn wir diese Aufgabe jede Sekunde im Modus mit fester Verzögerung planen möchten? Wir müssen die überladene Version von Schedule () verwenden, über die wir zuvor gesprochen haben:

new Timer().schedule(new NewsletterTask(), 0, 1000); for (int i = 0; i < 3; i++) { Thread.sleep(1000); }

Natürlich führen wir die Tests nur für wenige Fälle durch:

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

Wie wir sehen können, liegt zwischen jeder Ausführung mindestens eine Sekunde, die sich jedoch manchmal um eine Millisekunde verzögert. Dieses Phänomen ist auf unsere Entscheidung zurückzuführen, Wiederholungen mit fester Verzögerung zu verwenden.

3.2. Mit einer festen Rate

Was wäre, wenn wir eine Wiederholung mit fester Rate verwenden würden? Dann müssten wir die Methode ScheduledAtFixedRate () verwenden:

new Timer().scheduleAtFixedRate(new NewsletterTask(), 0, 1000); for (int i = 0; i < 3; i++) { Thread.sleep(1000); }

This time, executions are not delayed by the previous ones:

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.3. Schedule a Daily Task

Next, let's run a task once a day:

@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); }

4. Cancel Timer and TimerTask

An execution of a task can be canceled in a few ways:

4.1. Cancel the TimerTask Inside Run

By calling the TimerTask.cancel() method inside the run() method's implementation of the TimerTask itself:

@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); }

4.2. Cancel the Timer

By calling the Timer.cancel() method on a Timer object:

@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(); }

4.3. Stop the Thread of the TimerTask Inside Run

You can also stop the thread inside the run method of the task, thus canceling the entire task:

@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); }

Notice the TODO instruction in the run implementation – in order to run this simple example, we'll need to actually stop the thread.

In a real-world custom thread implementation, stopping the thread should be supported, but in this case we can ignore the deprecation and use the simple stop API on the Thread class itself.

5. Timer vs ExecutorService

You can also make good use of an ExecutorService to schedule timer tasks, instead of using the timer.

Here's a quick example of how to run a repeated task at a specified interval:

@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(); }

So what are the main differences between the Timer and the ExecutorService solution:

  • Timer can be sensitive to changes in the system clock; ScheduledThreadPoolExecutor is not
  • Timer has only one execution thread; ScheduledThreadPoolExecutor can be configured with any number of threads
  • Runtime Exceptions thrown inside the TimerTask kill the thread, so following scheduled tasks won't run further; with ScheduledThreadExecutor – the current task will be canceled, but the rest will continue to run

6. Conclusion

Dieses Tutorial erläutert die vielen Möglichkeiten Sie nutzen die leicht und flexibel machen Timer und Timertask - Infrastruktur aufgebaut in Java, für die schnelle Planung Aufgaben. Es gibt natürlich viel komplexere und vollständigere Lösungen in der Java-Welt, wenn Sie sie benötigen - wie die Quarzbibliothek -, aber dies ist ein sehr guter Ausgangspunkt.

Die Implementierung dieser Beispiele finden Sie im GitHub-Projekt - dies ist ein Eclipse-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.