En Java, la classe ScheduledExecutorService permet de planifier l'exécution périodique de tâches. Deux méthodes clés sont scheduleAtFixedRate et scheduleWithFixedDelay, qui diffèrent par la manière dont elles calculent l'intervalle entre les exécutions. scheduleAtFixedRate se base sur l'heure de début de la tâche précédente pour déterminer le prochain démarrage, tandis que scheduleWithFixedDelay utilise l'heure de fin de la tâche précédente.
Pour illustrer ce comportement, considérons un exemple où une série de tâches sont planifiées avec un délai de 5 secondes. Les tâches ont des durées d'exécution variables, et certaines peuvent prendre plus de temps que prévu. Voici un code réécrit qui simule cette situation en utilisant une approche différente pour la création et la gestion des tâches.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.logging.Logger;
public class ScheduledTaskDemo {
private static final Logger logger = Logger.getLogger(ScheduledTaskDemo.class.getName());
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
List<future>> futures = new ArrayList<>();
// Création d'une liste de tâches avec des propriétés différentes
List<map object="">> taskCollection = new ArrayList<>();
for (int index = 0; index < 10; index++) {
Map<string object=""> taskDetails = new HashMap<>();
taskDetails.put("label", "Task-" + (char)('A' + index));
taskDetails.put("duration", 1000);
taskCollection.add(taskDetails);
}
// Modifier la durée d'une tâche spécifique pour simuler un long traitement
taskCollection.get(5).put("duration", 15000);
logger.info("Détails des tâches : " + taskCollection);
// Planification avec scheduleAtFixedRate
for (Map<string object=""> task : taskCollection) {
ScheduledFuture> future = executor.scheduleAtFixedRate(() -> {
logger.info("Exécution de : " + task);
try {
int sleepTime = (int) task.get("duration");
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, 0, 5, TimeUnit.SECONDS);
futures.add(future);
}
// Attendre la fin de toutes les tâches (pour démonstration)
for (Future> future : futures) {
future.get();
}
executor.shutdown();
}
}
</string></string></map></future>
Dans cet exemple, les tâches sont planifiées pour démarrer immédiatement avec un intervalle fixe de 5 secondes. Cependant, comme le montre l'analyse des logs ci-dessous, le comportement réel dépend du temps d'exécution de chaque tâche et de la disponibilité des threads dans le pool.
Voici un extrait de logs simulant l'exécution avec scheduleAtFixedRate :
-- Les tâches initiales
[{label=Task-A, duration=1000}, {label=Task-B, duration=1000}, {label=Task-C, duration=1000}, ...]
-- Début des 5 premières tâches
11:06:06.505 [pool-1-thread-1] INFO ScheduledTaskDemo - Exécution de : {label=Task-A, duration=1000}
11:06:06.505 [pool-1-thread-2] INFO ScheduledTaskDemo - Exécution de : {label=Task-B, duration=1000}
...
-- Après 1s, les tâches courtes terminent et d'autres démarrent
11:06:07.505 [pool-1-thread-1] INFO ScheduledTaskDemo - Exécution de : {label=Task-G, duration=1000}
11:06:07.505 [pool-1-thread-2] INFO ScheduledTaskDemo - Exécution de : {label=Task-F, duration=15000}
...
-- Après 5s, les tâches initiales sont prêtes pour une seconde exécution
11:06:11.505 [pool-1-thread-1] INFO ScheduledTaskDemo - Exécution de : {label=Task-A, duration=1000}
...
-- La tâche longue bloque les exécutions ultérieures
11:06:22.507 [pool-1-thread-2] INFO ScheduledTaskDemo - Exécution de : {label=Task-F, duration=15000}
Les logs montrent que les tâches avec scheduleAtFixedRate tentent de respecter l'intervalle basé sur l'heure de début théorique. Par exemple, la tâche "Task-A" démarre à 11:06:06 et sa prochaine exécution est à 11:06:11, soit 5 secondes plus tard. Cependant, si une tâche prend trop de temps, les exécutions suivantes peuvent être retardées ou accumulées.
En examinant le code source de ScheduledThreadPoolExecutor, on observe que la méthode run de ScheduledFutureTask recalcule le temps de la prochaine exécution après chaque terminaison. Pour scheduleAtFixedRate, ce temps est basé sur le démarrage précédent :
public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
super.run();
else if (super.runAndReset()) {
setNextRunTime(); // Calcule la prochaine heure basée sur le démarrage
reExecutePeriodic(outerTask);
}
}
Une situation particulière se produit lorsque les durées d'exécution changent dynamiquement. Si on modifie la durée de toutes les tâches à 0 après la première exécution, scheduleAtFixedRate peut entraîner des exécutions consécutives rapides pour rattraper le retard. Par exemple, la tâche longue pourrait s'exécuter pluiseurs fois de suite.
Avec scheduleWithFixedDelay, l'intervalle est calculé à partir de la fin de la tâche précédente. Voici un extrait de logs simulé avec cette méthode :
-- Début similaire avec 5 tâches
11:51:51.450 [pool-1-thread-1] INFO ScheduledTaskDemo - Exécution de : {label=Task-A, duration=1000}
...
-- Après 5s à partir de la fin des tâches initiales
11:51:57.467 [pool-1-thread-1] INFO ScheduledTaskDemo - Exécution de : {label=Task-A, duration=1000}
...
-- La tâche longue retarde ses propres exécutions futures
11:52:12.471 [pool-1-thread-1] INFO ScheduledTaskDemo - Exécution de : {label=Task-F, duration=15000}
Ainsi, avec scheduleWithFixedDelay, la prochaine exécution est planifiée 5 secondes après la fin de la précédente, ce qui évite l'accumulation en cas de retards mais peut réduire la fréquence globale. En résumé, le choix entre ces deux méthodes dépend du besoin de maintenir un taux d'exécution constant ou de garantir un délai fixe entre les fins de tâches.