Java Timer

1. Timer: le basi

Timer e TimerTask sono classi java util utilizzate per pianificare le attività in un thread in background. In poche parole: TimerTask è l'attività da eseguire e Timer è lo scheduler .

2. Pianificare un'attività una volta

2.1. Dopo un determinato ritardo

Cominciamo semplicemente eseguendo una singola attività con l'aiuto di un 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); }

Ora, questo esegue l'attività dopo un certo ritardo , dato come secondo parametro del metodo schedule () . Vedremo nella prossima sezione come programmare un'attività in una determinata data e ora.

Nota che se stiamo eseguendo questo è un test JUnit, dovremmo aggiungere una chiamata Thread.sleep (delay * 2) per consentire al thread del timer di eseguire l'attività prima che il test Junit smetta di essere eseguito.

2.2. In una determinata data e ora

Ora, vediamo il metodo Timer # schedule (TimerTask, Date) , che prende una Data invece che una lunga come secondo parametro, permettendoci di programmare l'attività in un certo istante, piuttosto che dopo un ritardo.

Questa volta, immaginiamo di avere un vecchio database legacy e di voler migrare i suoi dati in un nuovo database con uno schema migliore.

Potremmo creare una classe DatabaseMigrationTask che gestirà quella migrazione:

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

Per semplicità, stiamo rappresentando i due database da un elenco di stringhe . In poche parole, la nostra migrazione consiste nel mettere i dati dal primo elenco nel secondo.

Per eseguire questa migrazione nell'istante desiderato, dovremo utilizzare la versione sovraccaricata del metodo schedule () :

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

Come possiamo vedere, assegniamo l'attività di migrazione e la data di esecuzione al metodo schedule () .

Quindi, la migrazione viene eseguita all'ora indicata da twoSecondsLater :

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

Mentre siamo prima di questo momento, la migrazione non si verifica.

3. Pianificare un'attività ripetibile

Ora che abbiamo spiegato come pianificare la singola esecuzione di un'attività, vediamo come gestire le attività ripetibili.

Ancora una volta, ci sono molteplici possibilità offerte dalla classe Timer : possiamo impostare la ripetizione per osservare un ritardo fisso o un tasso fisso.

Un ritardo fisso significa che l'esecuzione inizierà un periodo di tempo dopo il momento in cui è iniziata l'ultima esecuzione, anche se è stata ritardata (quindi essendo essa stessa ritardata) .

Supponiamo di voler pianificare un'attività ogni due secondi e che la prima esecuzione richiede un secondo e la seconda ne richiede due ma viene ritardata di un secondo. Quindi, la terza esecuzione inizierà al quinto secondo:

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

D'altra parte, un tasso fisso significa che ogni esecuzione rispetterà la pianificazione iniziale, indipendentemente dal fatto che un'esecuzione precedente sia stata ritardata .

Riutilizziamo il nostro esempio precedente, con una velocità fissa, la seconda attività inizierà dopo tre secondi (a causa del ritardo). Ma il terzo dopo quattro secondi (rispettando la pianificazione iniziale di un'esecuzione ogni due secondi):

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

Trattando questi due principi, vediamo come usarli.

Per poter utilizzare la pianificazione a ritardo fisso, ci sono altri due sovraccarichi del metodo schedule () , ciascuno dei quali accetta un parametro aggiuntivo che indica la periodicità in millisecondi.

Perché due sovraccarichi? Perché c'è ancora la possibilità di iniziare l'attività in un determinato momento o dopo un certo ritardo.

Per quanto riguarda la schedulazione a tasso fisso, abbiamo i due metodi scheduleAtFixedRate () che prendono anche una periodicità in millisecondi. Ancora una volta, abbiamo un metodo per avviare l'attività in una determinata data e ora e un altro per avviarla dopo un determinato ritardo.

Vale anche la pena ricordare che, se un'attività richiede più tempo del periodo di esecuzione, ritarda l'intera catena di esecuzioni sia che si utilizzi un ritardo fisso o un tasso fisso.

3.1. Con un ritardo fisso

Ora, immaginiamo di voler implementare un sistema di newsletter, inviando un'e-mail ai nostri follower ogni settimana. In tal caso, un compito ripetitivo sembra l'ideale.

Quindi, programmiamo la newsletter ogni secondo, che è fondamentalmente spam, ma poiché l'invio è falso, siamo a posto!

Progettiamo prima una NewsletterTask :

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

Ogni volta che viene eseguita, l'attività stamperà il suo orario pianificato, che raccogliamo utilizzando il metodo TimerTask # scheduleExecutionTime () .

Allora, cosa succede se vogliamo programmare questa attività ogni secondo in modalità a ritardo fisso? Dovremo usare la versione sovraccarica di schedule () di cui abbiamo parlato in precedenza:

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

Naturalmente, eseguiamo i test solo per alcune occorrenze:

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

Come possiamo vedere, c'è almeno un secondo tra ogni esecuzione, ma a volte sono ritardati di un millisecondo. Questo fenomeno è dovuto alla nostra decisione di utilizzare la ripetizione a ritardo fisso.

3.2. Con un tasso fisso

E se usassimo una ripetizione a tasso fisso? Quindi dovremmo usare il metodochedAtFixedRate () :

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

Questo tutorial ha illustrato i molti modi in cui è possibile utilizzare l' infrastruttura Timer e TimerTask semplice ma flessibile incorporata in Java, per pianificare rapidamente le attività. Ovviamente ci sono soluzioni molto più complesse e complete nel mondo Java se ne hai bisogno - come la libreria Quartz - ma questo è un ottimo punto di partenza.

L'implementazione di questi esempi può essere trovata nel progetto GitHub: questo è un progetto basato su Eclipse, quindi dovrebbe essere facile da importare ed eseguire così com'è.