• 6 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 30/11/2016

Améliorez la gestion de vos threads

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Depuis le début de ce cours, nous travaillons soit avec des objets héritant de l'objet Thread, soit avec des objets implémentant l'interface Runnable. Ces types d'objets sont très puissants, mais souffrent de deux problèmes :

  • ils ne peuvent pas lever d'exception;

  • ils ne retournent aucune valeur à la fin de leur traitement.

Afin de pallier à ces problèmes et de permettre une meilleure gestion de nos threads, deux interfaces ont vu le jour : il s'agit des interfaces Callable<V> et Future<V>.

En plus de ces interfaces, il existe des classes qui permettent de gérer un ensemble de threads afin de gagner en performance.

Nous allons parler de tout ça au long de ce chapitre.

Les interfaces Callable<V> et Future<V> : présentation

Comme vous l'aurez deviné, ces interfaces permettent de retourner un résultat après le traitement d'un thread, résultat de type <V> puisque ces interfaces utilisent les generics.
Callable<V> est une sœur très proche de l'interface Runnable à la différence près que Callable<V> retourne une valeur et lève une exception si le traitement ne peut être fait.

Cette interface ne contient qu'une seule méthode : public V call() throws Exception qui retourne donc un résultat générique.

Celle-ci travaille en étroite collaboration avec l'interface Future<V> qui propose une implémentation de base FutureTask<V>. Cet objet s'utilise conjointement avec un thread afin de lancer la tâche Callable<V> en arrière-plan et vous offre quelques méthodes utiles comme :

  • La méthode cancel(boolean mayInterruptIfRunning) : tente de mettre fin à la tâche, renvoie un booléen qui donne le résultat de cette tentative ;

  • La méthode get() : met le thread contenant cette invocation en attente jusqu'à ce que la tâche Callable<V> soit terminée. Elle retourne une valeur de type V ;

  • La méthode get(long timeout, TimeUnit unit) : idem que la méthode ci-dessus mais avec un délai d'expiration. Si ce délai est dépassé, une exception de type TimeoutException est levée ;

  • La méthode isCancelled() : retourne vrai si la méthode a été annulée avant la fin de son travail ;

  • La méthode isDone() : retourne vrai si la tâche s'est correctement déroulée.

Voici un code qui vous montre comment utiliser ces objets. Vous verrez, c'est très simple :

package com.sdz.callable1;

import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class Test2 {

   public static void main(String[] args){
      
      //Nous créons un objet Callable basique
      Callable<Integer> c1 = new Callable<Integer>(){
         public Integer call() throws Exception {            
            Random rand = new Random();
            int result = rand.nextInt(2_000);
            System.out.println("Dans l'objet Callable : " + result);
            try {
               Thread.sleep(3_000);
            } catch (Exception e) {
               e.printStackTrace();
            }
            return result;
         }
      };
      
      //nous l'associons à un objet FutureTask 
      //du même type générique
      FutureTask<Integer> ft1 = new FutureTask<>(c1);
            
      System.out.println(" - Lancement de notre premier test.");
      //Pour que cette tâche soit lancée dans un thread
      //nous devons tout de même utiliser la classe Thread
      //qui autorise un objet de type FutureTask dans son constructeur
      Thread t = new Thread(ft1);
      
      //Nous lançons maintenant le thread
      t.start();
      System.out.println("Traitement…");
      try {
         //Ici, notre objet Future attend la fin de la tâche pour
         //retourner le résultat, en attendant
         //le thread courant est bloqué
         System.out.println("Résultat : " + ft1.get());
      } catch (Exception e) {
         e.printStackTrace();
      }
      
      showStatus(ft1);
      
      System.out.println("\n - Lancement de notre second test.");
      ft1 = new FutureTask<>(c1);
      t = new Thread(ft1);
      t.start();
      System.out.println("Traitement…");
      
      //Ici, nous mettons un délai, il y aura donc une exception de levée
      //car le délai est inférieur à la pause dans l'objet Callable
      try {
         System.out.println("Résultat : " + ft1.get(500, TimeUnit.MILLISECONDS));
      } catch (InterruptedException | ExecutionException | TimeoutException e) {
         System.err.println("La tâche à mis trop de temps à répondre.");  
      }
      //Cette instruction n'affichera rien car le statut
      //de la tâche n'est ni OK ni annulée...
      showStatus(ft1);
      
   }  
   
   private static void showStatus(FutureTask<Integer> ft1){
      if(ft1.isDone())
         System.out.println("La tâche c'est déroulée correctement");
      
      if(ft1.isCancelled())
         System.out.println("La tâche a été annulée");
      
   }
}

Ce qui me donne ceci :

Exemple d'utilisation de Callable et Future
Exemple d'utilisation de Callable et Future

Vous voyez, rien de compliqué ici. Tout ressemble à ce que nous avons l'habitude de faire depuis le début de ce cours, nous utilisons juste deux objets au lieu d'un pour créer un thread.

Maintenant, je vous propose de voir une manière de grouper, gérer et planifier vos threads : le framework Executor.

La présentation continue : le framework Executor

Ce framework contient un ensemble de classes qui permet de créer et gérer un pool de threads.

Un quoi ?

Vous avez bien entendu, un pool de threads. Il s'agit d'un regroupement d'objets. On parle souvent de pool de connexions à une base de données et, ici, nous aurons un pool de threads.
Il s'agit donc d'un ensemble de threads, déjà créés, qui attendent de s'exécuter. Si vous vous demandez quel est l'intérêt d'avoir un pool, la réponse est qu'il est coûteux pour votre machine de créer un thread car sa création sollicite grandement celle-ci, donc cette opération prend un certain temps. Le fait d'avoir un pool de threads pré-créés permet d'avoir toujours à disposition un nombre de thread prêt à l'emploi sans avoir à redemander des ressources mémoire et processeur à votre machine. Mais avant tout, voyons de quoi est constitué ce framework et comment il fonctionne.

Ce framework vous met à disposition trois interfaces qui vous permettent de travailler avec des implémentations de Runnable, de Callable<V> ou des deux. Voici un diagramme de classes représentant ces trois interfaces, leurs relations et leurs principales méthodes :

Interfaces du framework Executor
Interfaces du framework Executor
  • l'interface Executor : permet d'exécuter des implémentations de l'interface Runnable uniquement ; 

  • l'interface ExecutorService : permet d'exécuter des implémentations des interfaces Runnable et Callable<V> ;

  • l'interface ScheduledExecutorService : permet d'exécuter des implémentations des interfaces Runnable et Callable<V> avec la possibilité de les planifier dans le temps ou de définir une périodicité (pouvoir faire en sorte que les tâches se répètent).

Ces interfaces vous permettent donc de gérer de façons différente vos tâches à lancer de façon asynchrone. Vous aurez donc le choix entre utiliser des implémentations de l'interface Runnable ou de Callable<V>, mais aussi le choix entre lancer vos tâches en différé ou de façon cyclique. Et ce framework offre encore d'autres fonctionnalités.

Comment pouvons-nous récupérer ces objets ? Il n'y a pas d'implémentation de base pour ceux-ci ?

En fait, il y a un objet qui se charge de vous fournir ces implémentations, l'objet Executors (n'oubliez pas le "S"…).
Il s'agit d'une factory qui vous offre une ribambelle de méthodes permettant d'obtenir des implémentations de ces interfaces. Voici les plus utilisées et les plus connues :

Elément

Explication

static <T> callable(Runnable task, T result)

Retourne un objet Callable<V> qui, une fois exécuté, lance la tâche en paramètre et retourne le résultat dans l'objet T .

newSingleThreadExecutor()

Crée une file d'attente de threads de type ExecutorService. Tous les threads seront exécutés dans un seul et unique thread, ce qui signifie que si quatre threads sont lancés, le second débutera dès que le premier aura terminé, le troisième débutera après la fin du deuxième thread etc.

newCachedThreadPool()

Crée un pool de thread où, lorsqu'un thread prend fin, ce dernier reste en cache pour être réutilisé si besoin. Cela permet d'éviter de re-créer un thread et donc de gagner en performance de temps d'exécution. La taille du pool grandit en fonction des besoins. Les threads mis en cache restent en attente pendant 60 secondes. Après ce délai, ils sont retirés du cache.

newFixedThreadPool(int nThreads)

Tout comme la méthode précédente à la différence près que le nombre de threads maximum est défini par le paramètre. Si le nombre de tâches demandées dépasse le nombre de threads disponibles, ces tâches sont mises en file d'attente.

newScheduledThreadPool(int corePoolSize)

Comme son nom l'indique, cette méthode crée un pool de threads qui accepte la planification et la périodicité. Les threads pourront ainsi être exécutés une fois ou plusieurs fois, immédiatement ou après un certain délai.

Tout ceci constitue le framework Executor. Maintenant que les présentations sont faites, je vous propose de passer à la pratique. :)

Executor en action !

Le moment tant attendu est enfin arrivé, nous allons pratiquer mais nous allons y aller doucement, un pas après l'autre. Je vous rassure tout de même, dès que nous aurons vu comment fonctionne un type de pool, le fonctionnement des autres sera quasiment identique. :)

Afin de bien voir les différences entre tous ces pools de threads, je vous propose de travailler sur un même exemple de code : un compteur de fichier.
Il s'agira d'un bête programme qui comptera le nombre de fichiers de X répertoires et qui nous donne le résultat total. Prêt ? En avant alors !

Il va donc nous falloir un objet qui scanne le contenu d'un dossier de façon récursive (en allant dans les sous-dossiers) et qui nous retourne le résultat trouvé.
Voici l'objet que nous allons utiliser :

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class FolderScanner implements Callable<Long>{
 
  private Path path = null;
  private long result = 0;
     
  public FolderScanner(){ }
  public FolderScanner(Path pf){
    path = pf;
  }
     
  /**
  * Méthode qui se charge de scanner les dossiers de façon récursive
  */
  public Long call(){
    
     System.out.println("Exécution dans " + Thread.currentThread().getName()); 
     System.out.println("Scan du dossier : " + path + " à la recherche des fichiers ");
         
      //On liste maintenant le contenu du répertoire pour traiter les sous-dossiers
      try(DirectoryStream<Path> listing = Files.newDirectoryStream(path)){           
        for(Path nom : listing){
          //S'il s'agit d'un dossier, on le scanne grâce à notre objet
          if(Files.isDirectory(nom.toAbsolutePath())){
            
            //si nous sommes dans un répertoire, nous lançons un nouveau thread d'exécution
            //sur ce répertoire pour compter le nombre de fichiers
            FolderScanner f = new FolderScanner(nom.toAbsolutePath());
            
            //On retrouve notre objet FutureTask qui va nous permettre de récupérer
            //le résultat du comptage de fichiers
            FutureTask<Long> ft = new FutureTask<>(f);
            Thread t = new Thread(ft);
            t.start();
            try {
               result += ft.get();
            } catch (InterruptedException e) {
               e.printStackTrace();
            } catch (ExecutionException e) {
               e.printStackTrace();
            }
          }
        }
      } catch (IOException e) { e.printStackTrace();}
         
      //Maintenant, on filtre le contenu de ce même dossier sur le filtre défini
      try(DirectoryStream<Path> listing = Files.newDirectoryStream(path)){
        for(Path nom : listing){
          //Pour chaque fichier correspondant, on incrémente notre compteur
          result++;
        }
      } catch (IOException e) { e.printStackTrace(); }
         
    return result;
  }
}

Nous allons maintenant voir comment utiliser les différents pools.

newSingleThreadExecutor()

Comme je vous l’avais dit, ce type de pool permet l'exécution d'un certain nombre de threads les uns après les autres, de façon séquentielle.
Je vous l'accorde, le parallélisme n'est pas son point fort mais ce genre de pool peut avoir son utilité dans certaines circonstances…

Voici comment on utilise ce pool :

Classe de test
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


public class SingleThread {

   public static void main(String[] args) {
      
      //Notre executor mono-thread
      ExecutorService execute = Executors.newSingleThreadExecutor();
      
      //Nous créons maintenant nos objets 
      Path chemin = Paths.get("C:\\Drivers");
      Path chemin2 = Paths.get("C:\\dell");
      Path chemin3 = Paths.get("C:\\Apps");
      
      //La méthode submit permet de récupérer un objet Future
      //qui contiendra le résultat obtenu
      Future<Long> ft1 = execute.submit(new FolderScanner(chemin));
      Future<Long> ft2 = execute.submit(new FolderScanner(chemin2));
      Future<Long> ft3 = execute.submit(new FolderScanner(chemin3));
      
      Long total;
      try {
         //Nous ajoutons tous les résultats
         total = ft1.get() + ft2.get() + ft3.get();
         System.out.println("nombre total de fichiers trouvés : " + total);
      } catch (InterruptedException | ExecutionException e) {
         e.printStackTrace();
      }
      
      //Dès que nos tâches sont terminées, nous fermons le pool
      //Sans cette ligne, ce programme restera en cours d'exécution
      execute.shutdown();
   }
}

Et voici le résultat :

Résultat de l'exécution
Résultat de l'exécution

Attends une seconde ! Tu nous as dit que ce pool n'exécutait les threads que de façon séquentielle… Mais nous avons plusieurs threads exécutés en même temps ici ?

Oui, je vous l'accorde mais, si vous regardez bien le résultat, le scan de chaque répertoire demandé est bien fait l'un après l'autre, dans un seul et unique thread :

Image utilisateur
Image utilisateur
Image utilisateur

Le lancement de chacune des trois tâches se fait bien dans un seul thread : pool-1-thread-1. Ce sont toutes les sous-tâches qui sont dans des threads séparés, donc nous scannons bien les répertoires les uns après les autres et le lancement du scan du deuxième répertoire attend bien que le scan du premier soit terminé ! :magicien:

Analysons ce code. je ne m'attarderai pas sur l'objet qui se charge de scanner les dossiers : il est très simple et vous devriez être à l'aise avec ce genre de choses maintenant. Voyons plutôt comment nous utilisons nos pools.

Tout d'abord, il faut le créer, via cette instruction :

ExecutorService execute = Executors.newSingleThreadExecutor();

Nous créons ensuite nos différentes tâches à effectuer, via notre objet qui scanne les dossiers puis nous les ajoutons dans notre pool, grâce à la méthode submit(Callable<T> callable) qui retourne un objet Future<T> qui contiendra le résultat de l'opération. Il ne nous reste plus qu'à récupérer les trois résultats, grâce à la méthode get() de l'objet Future<T>. Ensuite il faut absolument fermer le pool, via la méthode shutdown().

Voilà donc la marche à suivre pour gérer des pools de threads, les autres méthodes n'influent que sur leurs modes de fonctionnement : mise en cache des threads terminés, pool à taille fixe, exécution différée et cyclique…
Voici tout de même quelques codes d'exemples pour vous donner de l'inspiration, toujours en utilisant notre objet de base scannant des répertoires.

newCachedThreadPool()

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


public class CachedThreadPool {

   public static void main(String[] args) {
      
      //Notre executor
      ExecutorService execute = Executors.newCachedThreadPool();
      
      //Nous créons une liste stockant les objets Future<Long>
      ArrayList<Future<Long>> listFuture = new ArrayList<>();
      
      //Nous créons maintenant nos objets 
      Path chemin = Paths.get("C:\\Drivers");
      Path chemin2 = Paths.get("C:\\dell");
      Path chemin3 = Paths.get("C:\\Apps");
      
      //On change un peu le code en utilisant une boucle
      Path[] chemins = new Path[]{chemin, chemin2, chemin3};
      
      Long total = 0L;
      
      for(Path path : chemins){
         //Nous laçons le traitement
         Future<Long> ft = execute.submit(new FolderScanner(path));
         //Nous stockons l'objet Future<Long>
         //si nous avions utilisé la méthode get() directement
         //Les tâches se seraient lancées de façon séquentielle
         //car la méthode get() attend la fin du traitement
         listFuture.add(ft);         
      }
      
      //Afin d'avoir un traitement en parallèle
      //nous parcourons maintenant la liste de nos objets Future<T>
      Iterator<Future<Long>> it = listFuture.iterator();
      while(it.hasNext()){
         try {
            total += it.next().get();
         } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
         }
      }
      
      System.out.println("nombre total de fichiers trouvés : " + total);
      
      //Dès que nos tâches sont terminées, nous fermons le pool
      //Sans cette ligne, ce programme restera en cours d'exécution
      execute.shutdown();
   }
}

Ce code nous donne bien évidemment le même résultat que le précédent, à une différence près : le scan des trois dossiers se fait maintenant en parallèle. En voici la preuve :

Exécution en parallèles
Exécution en parallèle

Vous pouvez voir que, maintenant, trois threads sont utilisés dans le pool et que le scan des dossiers est bien fait de façon asynchrone.
Très simple et très efficace, vous en conviendrez. Passons donc à la suite.

newFixedThreadPool(int nThreads)

Ici, nous pouvons spécifier un nombre maximum de threads dans le pool. De ce fait, si nous spécifions un nombre maximum de threads à deux et que nous voulons effectuer une dizaine de tâches, les huit autres seront mises en attente et seront exécutées lorsque l'un des threads sera de nouveau disponible.

Voici un petit exemple :

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


public class FixedThreadPool {

   public static void main(String[] args) {
      
      //Notre executor fixé à 2 threads maximum
      ExecutorService execute = Executors.newFixedThreadPool(2);
      
      //Nous créons une liste stockant les objets Future<Long>
      ArrayList<Future<Long>> listFuture = new ArrayList<>();
      
      //Nous créons maintenant nos objets 
      Path chemin = Paths.get("C:\\Drivers");
      Path chemin2 = Paths.get("C:\\dell");
      Path chemin3 = Paths.get("C:\\Apps");
      Path chemin4 = Paths.get("C:\\Partage");
      
      //On change un peu le code en utilisant une boucle
      Path[] chemins = new Path[]{chemin, chemin2, chemin3, chemin4};
      
      Long total = 0L;
      
      for(Path path : chemins){
         //Nous laçons le traitement
         Future<Long> ft = execute.submit(new FolderScanner(path));
         //Nous stockons l'objet Future<Long>
         //si nous avions utilisé la méthode get() directement
         //Les tâches se seraient lancées de façon séquentielle
         //car la méthode get() attend la fin du traitement
         listFuture.add(ft);         
      }
      
      //Afin d'avoir un traitement en parallèle
      //nous parcourons maintenant la liste de nos objets Future<T>
      Iterator<Future<Long>> it = listFuture.iterator();
      while(it.hasNext()){
         try {
            total += it.next().get();
         } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
         }
      }
      
      System.out.println("nombre total de fichiers trouvés : " + total);
      
      //Dès que nos tâches sont terminées, nous fermons le pool
      //Sans cette ligne, ce programme restera en cours d'exécution
      execute.shutdown();
   }
}

Et pour vous montrer que seul deux threads sont bien utilisés, voici quelques captures d'écran.
Au lancement du programme, vu que seuls deux threads simultanés sont autorisés, nous avons deux lancements :

Les deux threads maximums lancés
Les deux threads maximums lancés

Ensuite, après quelques temps, un thread termine sont travail et laisse la place à la tâche suivante, dans mon cas, il s'agit de pool-1-thread-2 qui accueille maintenant le scan du dossier "C:\Apps":

Image utilisateur

Ensuite, un autre thread se libère, laissant ainsi la place à notre dernière tâche :

Image utilisateur

Nous avons donc bien bloqué le nombre de threads maximum en cours d'exécution dans notre pool !

newScheduledThreadPool(int corePoolSize)

Ce dernier type de pool permet de planifier des exécutions dans le temps. Nous pouvons donc lui dire de lancer une tâche après un certain temps, mais aussi de répéter cette dernière un certain nombre de fois.
En fait, il y a deux modes de fonctionnement bien distincts :

  • Un mode qui permet de lancer des tâches après un certain temps d'attente, puis de de récupérer un résultat (si vous utilisez des objets Callable<T>) ou bien de ne rien récupérer (si vous utilisez des objets Runnable).

  • Un mode qui permet de lancer des tâches après un temps d'attente et de les relancer régulièrement après un certain laps de temps. Mais ici, il ne sera pas possible de récupérer un résultat car ces méthodes ne fonctionnent qu'avec des objet de type Runnable.

Je vous propose de voir tout ceci dans les grandes lignes.

Utilisation d'un pool à exécution différée

Voici un code d'exemple :

package Scheduled.Callable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;


public class ScheludedThreadPool {
   

   public static void main(String[] args) {
      
      //Cette instruction permet de lister le nombre de processeurs disponibles
      //sur la machine exécutant le programme      
      int corePoolSize = Runtime.getRuntime().availableProcessors();
      System.out.println("Nombre de processeurs disponibles : " + corePoolSize);
      
      //Notre executor avec un nombre de processeurs fixés dynamiquement 
      ScheduledExecutorService execute = Executors.newScheduledThreadPool(corePoolSize);
      
      //Nous créons une liste stockant les objets Future<Long>
      ArrayList<Future<Long>> listFuture = new ArrayList<>();
      
      //Nous créons maintenant nos objets 
      Path chemin = Paths.get("C:\\Drivers");
      Path chemin2 = Paths.get("C:\\dell");
      Path chemin3 = Paths.get("C:\\Partage");
      
      Long total = 0L;
            
      //Ici, nous lançons la tâche N° 1 dans 10 secondes
      Future<Long> ft = execute.schedule(new FolderScanner(chemin), 10, TimeUnit.SECONDS);
      listFuture.add(ft);
      
      //Ici, nous lançons la tâche N° 2 dans 1 secondes
      Future<Long> ft2 = execute.schedule(new FolderScanner(chemin2), 1000, TimeUnit.MILLISECONDS);
      listFuture.add(ft2);
      
      //Ici, nous lançons la tâche N° 3 dans 1 minute
      Future<Long> ft3 = execute.schedule(new FolderScanner(chemin3), 1, TimeUnit.MINUTES);
      listFuture.add(ft3);
      
      
      //Afin d'avoir un traitement en parallèle
      //nous parcourons maintenant la liste de nos objets Future<T>
      Iterator<Future<Long>> it = listFuture.iterator();
      while(it.hasNext()){
         try {
            total += it.next().get();
         } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
         }
      }
      
      System.out.println("nombre total de fichiers trouvés : " + total);
      
      
      //Dès que nos tâches sont terminées, nous fermons le pool
      //Sans cette ligne, ce programme restera en cours d'exécution
      execute.shutdown();
   }
}

Après exécution de ce dernier, vous devriez vous rendre compte que les tâches sont bien exécutées et qu'elles débutent en fonction des indications que nous avons fournies dans la méthode schedule(). Il n'y a rien de compliqué ici, le premier argument de cette méthode correspond à la tâche que nous souhaitons lancer, le second au délai avant le lancement de la tâche et le dernier à l'unité de temps pour le délai.
Ainsi, dans ce code, la tâche 2 se lance en premier, ensuite la tâche 1, et enfin, au bout d'une minute, la tâche 3.

Utilisation d'un pool à exécution différée avec répétition de tâche

Ici, nous n'aurons plus de résultat retourné car chaque tâche sera répétée indéfiniment jusqu'à ce que le programme s'arrête. Nous allons devoir modifier un peu notre objet FolderScanner afin qu'il implémente l'interface Runnable. Rien de compliqué : nous ajoutons simplement Runnable dans la liste des interfaces à implémenter et nous ajoutons la méthode run() en fin de classe, comme ceci :

package Scheduled.Runnable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;


public class ScheludedThreadPool {
   

   public static void main(String[] args) {
      
      //Cette instruction permet de lister le nombre de processeurs disponibles
      //sur la machine exécutant le programme      
      int corePoolSize = Runtime.getRuntime().availableProcessors();
      System.out.println("Nombre de processeurs disponibles : " + corePoolSize);
      
      //Notre executor avec un nombre de processeurs fixés dynamiquement 
      ScheduledExecutorService execute = Executors.newScheduledThreadPool(corePoolSize);
      
      //Nous créons maintenant nos objets 
      Path chemin = Paths.get("C:\\Drivers");
      Path chemin2 = Paths.get("C:\\dell");
      Path chemin3 = Paths.get("C:\\Partage");
                        
      //Ici, nous lançons la tâche N° 3 dans 40 secondes
      //elle se répétera toutes les 20 secondes
      execute.scheduleAtFixedRate(new FolderScanner(chemin3), 40_000, 20_000, TimeUnit.MILLISECONDS);
      
      //Ici, nous lançons la tâche N° 2 dans 30 secondes
      //elle se répétera toutes les 30 secondes
      execute.scheduleAtFixedRate(new FolderScanner(chemin2), 30, 30, TimeUnit.SECONDS);
      
      //Ici, nous lançons la tâche N° 1 dans 1 minute
      //elle se répétera toutes les minutes
      execute.scheduleWithFixedDelay(new FolderScanner(chemin), 1, 1, TimeUnit.MINUTES);
      
      //Si nous fermons le pool, les tâches seront abandonnées
      //j'ai donc commenté la ligne
      //execute.shutdown();
   }
}

Ce qui me donne ce résultat :

Résultat de la planification
Résultat de la planification

Vous pouvez constater que les threads sont bien lancés à intervalles réguliers et que ceux-ci continuent d'être exécutés à l'infini…
Il y a bien trois scans du dossier "C:\Partage" et deux scans du dossier "C:\dell" entre deux scans du dossier "C:\Drivers".

Ceci est très utile pour des tâches à planifier. ;)

Maintenant que vous savez bien mieux gérer vos threads, je vous propose de finir ce cours avec une séance de perfectionnement sur une autre notion importante : les Synchronyzers.

Exemple de certificat de réussite
Exemple de certificat de réussite