Minuterie Java

1. Minuterie - les bases

Timer et TimerTask sont des classes utilitaires java utilisées pour planifier des tâches dans un thread d'arrière-plan. En quelques mots - TimerTask est la tâche à effectuer et Timer est le planificateur .

2. Planifiez une tâche une fois

2.1. Après un délai donné

Commençons par simplement exécuter une seule tâche à l'aide d'un minuteur :

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

Maintenant, cela exécute la tâche après un certain délai , donné comme deuxième paramètre de la méthode schedule () . Nous verrons dans la section suivante comment planifier une tâche à une date et une heure données.

Notez que si nous exécutons ceci est un test JUnit, nous devons ajouter un appel Thread.sleep (delay * 2) pour permettre au thread du Timer d'exécuter la tâche avant que le test Junit ne s'arrête.

2.2. À une date et une heure données

Voyons maintenant la méthode Timer # schedule (TimerTask, Date) , qui prend une Date au lieu d'un long comme deuxième paramètre, ce qui nous permet de planifier la tâche à un certain instant, plutôt qu'après un délai.

Cette fois, imaginons que nous ayons une ancienne base de données héritée et que nous souhaitons migrer ses données dans une nouvelle base de données avec un meilleur schéma.

Nous pourrions créer une classe DatabaseMigrationTask qui gérera cette migration:

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

Pour plus de simplicité, nous représentons les deux bases de données par une liste de chaînes . En termes simples, notre migration consiste à placer les données de la première liste dans la seconde.

Pour effectuer cette migration à l'instant souhaité, nous devrons utiliser la version surchargée de la méthode 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);

Comme nous pouvons le voir, nous donnons la tâche de migration ainsi que la date d'exécution à la méthode schedule () .

Ensuite, la migration est exécutée à l'heure indiquée par twoSecondsLater :

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

Bien que nous soyons avant ce moment, la migration ne se produit pas.

3. Planifier une tâche répétable

Maintenant que nous avons expliqué comment planifier l'exécution unique d'une tâche, voyons comment gérer les tâches répétables.

Encore une fois, les possibilités offertes par la classe Timer sont multiples : nous pouvons configurer la répétition pour observer soit un retard fixe, soit un taux fixe.

Un délai fixe signifie que l'exécution commencera une période de temps après le moment où la dernière exécution a commencé, même si elle a été retardée (donc étant elle-même retardée) .

Disons que nous voulons planifier une tâche toutes les deux secondes, et que la première exécution prend une seconde et la seconde prend deux mais est retardée d'une seconde. Ensuite, la troisième exécution commencerait à la cinquième seconde:

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

D'autre part, un taux fixe signifie que chaque exécution respectera le calendrier initial, peu importe si une exécution précédente a été retardée .

Réutilisons notre exemple précédent, avec un taux fixe, la deuxième tâche démarrera au bout de trois secondes (à cause du retard). Mais, le troisième après quatre secondes (en respectant le planning initial d'une exécution toutes les deux secondes):

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

Ces deux principes étant couverts, voyons comment les utiliser.

Afin d'utiliser l'ordonnancement à délai fixe, il existe deux surcharges supplémentaires de la méthode schedule () , chacune prenant un paramètre supplémentaire indiquant la périodicité en millisecondes.

Pourquoi deux surcharges? Parce qu'il y a encore la possibilité de démarrer la tâche à un certain moment ou après un certain délai.

En ce qui concerne l'ordonnancement à taux fixe, nous avons les deux méthodes scheduleAtFixedRate () prenant également une périodicité en millisecondes. Encore une fois, nous avons une méthode pour démarrer la tâche à une date et une heure données et une autre pour la démarrer après un délai donné.

Il convient également de mentionner que si une tâche prend plus de temps que la période à exécuter, elle retarde toute la chaîne d'exécutions, que nous utilisions un délai fixe ou un taux fixe.

3.1. Avec un délai fixe

Maintenant, imaginons que nous voulons mettre en place un système de newsletter, envoyant un email à nos followers chaque semaine. Dans ce cas, une tâche répétitive semble idéale.

Alors, programmons la newsletter toutes les secondes, ce qui est essentiellement du spam, mais comme l'envoi est faux, nous sommes prêts à partir!

Première conception d' un Let NewsletterTask :

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

Chaque fois qu'elle s'exécute, la tâche imprimera son heure planifiée, que nous collectons à l'aide de la méthode TimerTask # planifiéExecutionTime () .

Ensuite, que se passe-t-il si nous voulons planifier cette tâche toutes les secondes en mode à délai fixe? Nous devrons utiliser la version surchargée de schedule () dont nous avons parlé plus tôt:

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

Bien sûr, nous ne réalisons les tests que pour quelques 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

Comme on peut le voir, il y a au moins une seconde entre chaque exécution, mais elles sont parfois retardées d'une milliseconde. Ce phénomène est dû à notre décision d'utiliser la répétition à délai fixe.

3.2. Avec un taux fixe

Maintenant, et si nous devions utiliser une répétition à taux fixe? Ensuite, nous devrions utiliser la méthode planifiéeAtFixedRate () :

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

Ce didacticiel a illustré les nombreuses façons dont vous pouvez utiliser l' infrastructure Timer et TimerTask, simple mais flexible , intégrée à Java, pour planifier rapidement les tâches. Il existe bien sûr des solutions beaucoup plus complexes et complètes dans le monde Java si vous en avez besoin - comme la bibliothèque Quartz - mais c'est un très bon point de départ.

L'implémentation de ces exemples se trouve dans le projet GitHub - il s'agit d'un projet basé sur Eclipse, il devrait donc être facile à importer et à exécuter tel quel.