Java-timer

1. Timer - grunderna

Timer och TimerTask är java util-klasser som används för att schemalägga uppgifter i en bakgrundstråd. Med några ord - TimerTask är uppgiften att utföra och Timer är schemaläggaren .

2. Schemalägg en uppgift en gång

2.1. Efter en given fördröjning

Låt oss börja med att helt enkelt köra en enda uppgift med hjälp av en timer :

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

Nu utför detta uppgiften efter en viss fördröjning , ges som den andra parametern för schemat () -metoden. Vi kommer att se i nästa avsnitt hur man schemalägger en uppgift vid ett visst datum och tid.

Observera att om vi kör detta är ett JUnit-test, bör vi lägga till ett Thread.sleep (fördröjning * 2) -anrop för att låta timerns tråd köra uppgiften innan Junit-testet slutar köra.

2.2. Vid ett givet datum och tid

Låt oss nu se metoden Timer # schema (TimerTask, Date) , som tar ett datum istället för länge som sin andra parameter, så att vi kan schemalägga uppgiften vid ett visst ögonblick snarare än efter en fördröjning.

Den här gången, låt oss föreställa oss att vi har en gammal äldre databas och vi vill migrera dess data till en ny databas med ett bättre schema.

Vi kan skapa en DatabaseMigrationTask- klass som hanterar migrationen:

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

För enkelhetens skull representerar vi de två databaserna med en lista över strängar . Enkelt uttryckt består vår migration av att placera data från den första listan i den andra.

För att utföra denna migrering vid önskad tidpunkt måste vi använda den överbelastade versionen av metoden schema () :

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

Som vi kan se ger vi migreringsuppgiften samt datum för körning till schemat () -metoden.

Därefter körs migreringen vid den tidpunkt som anges av twoSecondsLater :

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

Medan vi befinner oss före det här ögonblicket sker inte migrationen.

3. Schemalägg en upprepbar uppgift

Nu när vi har beskrivit hur man schemalägger en enskild körning av en uppgift, låt oss se hur man hanterar upprepningsbara uppgifter.

Återigen finns det flera möjligheter som Timer- klassen erbjuder : Vi kan ställa in repetitionen för att observera antingen en fast fördröjning eller en fast hastighet.

En fast fördröjning innebär att körningen kommer att börja en tid efter det att den senaste körningen startade, även om den var försenad (därför själv fördröjd) .

Låt oss säga att vi vill schemalägga någon uppgift varannan sekund och att den första körningen tar en sekund och den andra tar två men försenas med en sekund. Därefter skulle den tredje körningen börja vid den femte sekunden:

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

Å andra sidan innebär en fast ränta att varje körning respekterar det ursprungliga schemat, oavsett om ett tidigare utförande har försenats .

Låt oss återanvända vårt tidigare exempel, med en fast ränta, den andra uppgiften börjar efter tre sekunder (på grund av förseningen). Men den tredje efter fyra sekunder (respekterar det ursprungliga schemat för en körning varannan sekund):

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

Dessa två principer täcks, låt oss se hur man använder dem.

För att använda schemaläggning med fast fördröjning finns det ytterligare två överbelastningar av schemat () -metoden, var och en tar en extra parameter som anger periodiciteten i millisekunder.

Varför två överbelastningar? Eftersom det fortfarande finns möjlighet att starta uppgiften vid ett visst ögonblick eller efter en viss fördröjning.

As for the fixed-rate scheduling, we have the two scheduleAtFixedRate() methods also taking a periodicity in milliseconds. Again, we've got one method to start the task at a given date and time and another to start it after a given delay.

It's also worth mentioning that, if a task takes more time than the period to execute, it delays the whole chain of executions whether we're using fixed-delay or fixed-rate.

3.1. With a Fixed Delay

Now, let's imagine we want to implement a newsletter system, sending an email to our followers every week. In that case, a repetitive task seems ideal.

So, let's schedule the newsletter every second, which is basically spamming, but as the sending is fake we're good to go!

Let's first design a NewsletterTask:

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

Each time it executes, the task will print its scheduled time, which we gather using the TimerTask#scheduledExecutionTime() method.

Then, what if we want to schedule this task every second in fixed-delay mode? We'll have to use the overloaded version of schedule() we talked about earlier:

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

Of course, we only carry the tests for a few occurrences:

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

As we can see, there is at least one second between each execution, but they are sometimes delayed by a millisecond. That phenomenon is due to our decision to used fixed-delay repetition.

3.2. With a Fixed Rate

Now, what if we were to use a fixed-rate repetition? Then we would've to use the scheduledAtFixedRate() method:

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

Denna handledning illustrerar de många sätten du kan använda den enkla men ändå flexibla Timer- och TimerTask- infrastrukturen inbyggd i Java för snabb schemaläggning av uppgifter. Det finns naturligtvis mycket mer komplexa och kompletta lösningar i Java-världen om du behöver dem - som Quartz-biblioteket - men det här är ett mycket bra ställe att börja.

Implementeringen av dessa exempel finns i GitHub-projektet - detta är ett Eclipse-baserat projekt, så det bör vara enkelt att importera och köra som det är.