Tutoriel sur la programmation concurrente en Java

Partie 1 : Gestion des threads

La programmation concurrente est un enjeu important et parfois difficile pour les développeurs. Cette série d'articles vise à vous présenter les différentes API disponibles en standard avec Java.

Les articles de la série :

Ce premier article sert de préambule afin de poser les bases de la programmation parallèle en Java, à savoir la gestion des threads.

8 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Présentation

Les ordinateurs ont vu leur nombre de processeurs/cœurs augmentés grandement ces dernières années afin de compenser les limites d'augmentation de puissance d'un seul processeur. Afin d'exploiter pleinement cette puissance de calcul, il convient de réaliser des applications capables d'effectuer plusieurs tâches en parallèle. Et même si les systèmes d'exploitation sont depuis longtemps multitâches, Java demeure un langage séquentiel à sa base.

Depuis longtemps, les développeurs utilisent deux techniques pour rendre leurs applications multitâches : le fork ou les threads. En Java, c'est cette dernière technique qui est utilisée. Contrairement au fork, les threads partagent la même mémoire.

Partageant ainsi des ressources (en mémoire), les threads risquent alors de rentrer en concurrence et de corrompre le système. C'est alors qu'intervient la programmation concurrente qui rassemble un ensemble de fonctionnalités et de techniques pour permettre la synchronisation de tâches s'exécutant en parallèle.

Cet article s'attache uniquement à la synchronisation des ressources en mémoire. En effet, les autres ressources étant partagées également par d'autres processus (éventuellement sur d'autres machines), les outils et techniques sont souvent propres à chaque ressource (base de données, disque, etc.).

Si ces ressources sont accédées uniquement par plusieurs threads du même programme Java, les outils et techniques décrits ci-après s'appliquent dans la mesure où vous créez un objet unique pour chacune de ces ressources.

II. Modèle

En Java, un thread est représenté par la classe java.lang.Thread. Les threads disposent de plusieurs propriétés :

  • id : un numéro unique ;
  • name : le nom du thread. Il n'est pas nécessairement unique ;
  • priority : la machine virtuelle exécute prioritairement les threads avec la plus grande valeur ;
  • state : l'état du thread ;
  • threadGroup : permet d'organiser les threads ;
  • daemon : indique si le thread est une tâche de fond (démon). La VM s'arrête automatiquement dès lors qu'aucun thread principal (non démon) n'est en cours d'exécution.

Les groupes de threads forment une arborescence. Lors de sa création, par défaut, un groupe appartient au groupe du thread en cours et hérite également de ses propriétés. Il en est de même pour les threads qui héritent des propriétés du thread en cours.

Pour exécuter du code dans un thread, il existe deux techniques :

  1. Créer une classe fille de Thread et surcharger la méthode run() :

    Logger
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    41.
    42.
    43.
    44.
    45.
    //com.developpez.lmauzaize.java.concurrence.Logger
    public class Logger {
    
      private static final long startTime = System.currentTimeMillis();
      private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
      public static final UncaughtExceptionHandler exceptionHandler = new UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
          printStackTrace(t, e);
        }
        public String toString() {
          return "Logger.printStackTrace(Thread,Throwable)";
        }
      };
      private Logger() {}
    
      
      public static void println(String msg, Object... params) {
        println(Thread.currentThread(), msg, params);
      }
      public static void println(Thread thread, String msg, Object... params) {
        // Message
        String format = "%tT.%<tL [%-15s] " + msg + "%n";
    
        // Paramètres
        Object[] args = new Object[2 + (params != null ? params.length : 0)];
        Calendar cal = Calendar.getInstance(UTC);
        cal.setTimeInMillis(System.currentTimeMillis() - startTime);
        args[0] = cal;
        args[1] = Thread.currentThread().getName();
        if (args.length > 2) {
          System.arraycopy(params, 0, args, 2, params.length);
        }
        System.out.printf(format, args);
      }
    
      public static void printStackTrace(Throwable t) {
        printStackTrace(Thread.currentThread(), t);
      }
      public static void printStackTrace(Thread thread, Throwable t) {
        StringWriter writer = new StringWriter();
        t.printStackTrace(new PrintWriter(writer, true));
        println(thread, "%s%n%s", t, writer);
      }
    }
    

    La classe Logger sera utilisée tout au long de cet article pour faciliter l'affichage des traces sur la sortie standard. Vous noterez également que la première ligne est un commentaire indiquant le nom complet de la classe dans les sources de l'article.

    Nouvelle classe fille de Thread
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.FilleThread
    new Thread() {
      public void run() {
        Logger.println("Je suis un nouveau thread !");
      }
    }.start();
    
  2. Créer une instance de Runnable et passer cette instance en paramètre lors de la création du thread. Cette dernière technique permet d'exécuter plusieurs fois le même code :

    Nouvelle classe Runnable
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.NouveauRunnable
    Runnable runnable = new Runnable() {
      public void run() {
        Logger.println("Je suis un nouveau thread !");
      }
    };
    new Thread(runnable).start();
    new Thread(runnable).start();
    

Certaines parties de l'API utilisent une classe similaire à Runnable qui permet à une tâche de renvoyer un résultat, il s'agit de l'interface Callable.

Pour terminer cette présentation des spécifications Java, il est important de noter que toute instance peut servir de verrou. Ce qui nous amène à présenter les blocs synchronized et les méthodes wait/notify.

III. Gestion des threads

La section Modèle présentait le fonctionnement de base des threads avec Java. Cette section vise à présenter les solutions proposées par l'API pour gérer l'exécution de tâches sans avoir à gérer manuellement les threads.

III-A. ThreadFactory

Bien que la gestion des threads puisse être déléguée à un service dédié (voir ci-après), leur création fait l'objet d'une interface particulière appelée ThreadFactory. Cette interface permet de spécifier les paramètres des threads à créer : groupe, nom, priorité, démon, etc.

L'API fournit deux méthodes pour créer des ThreadFactory :

  • defaultThreadFactory : créer des threads avec les caractéristiques décrites ci-dessous :

    • Nom : de la forme « pool-<N>-thread-<M> ». « N » étant un numéro séquentiel de fabrique et « M » un numéro séquentiel de thread au sein de la fabrique,
    • Groupe : le groupe est déterminé à la création de la fabrique. S'il y a un gestionnaire de sécurité, c'est son groupe qui est utilisé ; sinon c'est celui du thread courant,
    • Démon : la fabrique ne produit que des threads non démons,
    • Priorité : la priorité est normale sauf si le maximum toléré par le groupe est inférieur.
    Executors.defaultThreadFactory - démo (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    41.
    42.
    43.
    44.
    45.
    46.
    47.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.threadfactory.DefaultThreadFactoryDemo
    public static Thread créerThreadLimité(Runnable tâche, String name) {
      ThreadGroup groupe = new ThreadGroup("groupe");
      groupe.setDaemon(true);
      groupe.setMaxPriority(Thread.MIN_PRIORITY);
      Thread thread = new Thread(groupe, tâche, name);
      thread.setDaemon(true);
      class FauxClassLoader extends ClassLoader {}
      thread.setContextClassLoader(new FauxClassLoader());
      thread.setUncaughtExceptionHandler(Logger.exceptionHandler);
      return thread;
    }
    
    public static void affiche(String cas, Thread) {
        Logger.println("");
        Logger.println(cas);
        Logger.println("  nom     =%s", thread.getName());
        Logger.println("  groupe  =%s", thread.getThreadGroup().getName());
        Logger.println("  démon   =%s", thread.isDaemon());
        Logger.println("  priorité=%s", thread.getPriority());
        Logger.println("  loader  =%s", thread.getContextClassLoader());
        Logger.println("  handler =%s", thread.getUncaughtExceptionHandler());
    }
    
    public static void main(String[] args) throws InterruptedException {
    
      ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory();
      ThreadFactory customThreadFactory  = Thread::new;
    
      Runnable tâche = () -> {
        affiche("defaultThreadFactory", defaultThreadFactory.newThread(null));
        affiche("customThreadFactory" , customThreadFactory.newThread(null));
      };
      
      // Depuis "main"
      tâche.run();
    
      // Depuis un nouveau thread
      Thread defaultThread = defaultThreadFactory.newThread(tâche);
      defaultThread.start();
      defaultThread.join();
    
      // Depuis un groupe restreint
      Thread thread = créerThreadLimité(tâche, "thread");
      thread.start();
      thread.join();
    }
    
    Executors.defaultThreadFactory - démo (console)
    CacherSélectionnez
  • privilegedThreadFactory : même chose que précédemment avec le ClassLoader et les mêmes permissions que le thread ayant créé la fabrique.

    Executors.privilegedThreadFactory - démo (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    41.
    42.
    43.
    44.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.threadfactory.PrivilegedThreadFactoryDemo
    // Vérifie si le thread possède les permissions
    static boolean possèdePermission();
    // Effectue une action avec les permissions
    static <T> T avecPermission(PrivilegedAction<T> action);
    
    public static void afficheAvecPermission(String cas) {
      affiche(cas, Thread.currentThread());
      Logger.println("  permis  =%s", possèdePermission());
    }
    
    class Executeur implements Runnable {
    
      Map<String, ThreadFactory> threadFactories = new LinkedHashMap<>();
      Executeur() {
        threadFactories.put("Thread.new"             , Thread::new);
        threadFactories.put("defaultThreadFactory"   , Executors.defaultThreadFactory());
        threadFactories.put("privilegedThreadFactory", Executors.privilegedThreadFactory());
      }
    
      public void run() {
        for (Entry<String, ThreadFactory> threadFactory : threadFactories.entrySet()) {
          Thread thread = threadFactory.getValue().newThread(() -> afficheAvecPermission(threadFactory.getKey()));
          thread.start();
          thread.join();
        }
      }
    }
    
    Logger.println("");
    Logger.println("");
    Logger.println("******************************");
    Logger.println("Sans permission");
    new Executeur().run();
    
    Logger.println("");
    Logger.println("");
    Logger.println("******************************");
    Logger.println("Avec permission");
    Executeur executeur = avecPermission(Executeur::new);
    
    Thread thread = créerThreadLimité(executeur, "tâche");
    thread.start();
    thread.join();
    
    Executors.privilegedThreadFactory - démo (console)
    CacherSélectionnez

III-B. ExecutorService

L'interface Executor désigne un service capable de traiter des tâches (Runnable). Elle est étendue par l'interface ExecutorService qui permet de suivre l'évolution du traitement des tâches via des Future (voir ci-après).

Pour créer une instance d'ExecutorService, le plus simple est d'utiliser la classe utilitaire Executors :

  • newCachedThreadPool : utilise un pool de threads qui grandit en fonction des besoins. Les threads créés sont conservés jusqu'à 60 secondes d'inactivité. Il est donc idéal pour exécuter plusieurs tâches courtes, mais pas pour exécuter un grand nombre de tâches en parallèle, en effet il risque de saturer le système par la création d'un trop grand nombre de threads.

    ThreadFactory - logger
    CacherSélectionnez
    Executors.newCachedThreadPool - démo (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.executors.NewCachedThreadPoolDemo
    int parallele = 4;
    
    ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactoryLogger());
    for (int i = 1; i <= parallele; i++) {
      String nom = String.format("Tache-%02d", i);
      Runnable tache = new Runnable() {
        public void run() {
          Logger.println("[%s] Début", nom);
          int pause = ThreadLocalRandom.current().nextInt(2_000) + 200;
          Logger.println("[%s] Pause %dms", nom, pause);
          TimeUnit.MILLISECONDS.sleep(pause);
          Logger.println("[%s] Fin", nom);
        }
      };
      executor.submit(tache);
    }
    
    Executors.newCachedThreadPool - démo (console)
    CacherSélectionnez
  • newFixedThreadPool : utilise un pool de threads de taille fixe. Les threads sont démarrés au fur et à mesure des besoins jusqu'à ce que le pool atteigne sa taille maximale. Les threads ainsi démarrés vivent tant que le service n'est pas arrêté. Si tous les threads sont occupés, les tâches sont mises en attente dans une file pour une exécution ultérieure.

    Executors.newFixedThreadPool - démo (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.executors.FixedThreadPoolDemo
    int parallele = 4;
    
    Logger.println("Lancement du pool");
    ExecutorService executor = Executors.newFixedThreadPool(parallele, new ThreadFactoryLogger());
    for (int i = 1; i <= parallele * 2; i++) {
      String nom = String.format("Tache-%02d", i);
      Logger.println("Soumission de la tâche %s", nom);
      int nb = i;
      Runnable tache = new Runnable() {
        public void run() {
          int pause = ThreadLocalRandom.current().nextInt(2_000) + 1_500 + (parallele*2-nb)*500;
          Logger.println("[%s] Pause %dms", nom, pause);
          TimeUnit.MILLISECONDS.sleep(pause);
          Logger.println("[%s] Fin", nom);
        }
      };
      executor.submit(tache);
      TimeUnit.MILLISECONDS.sleep(500);
    }
    TimeUnit.SECONDS.sleep(12);
    executor.shutdown();
    }
    
    Executors.newFixedThreadPool - démo (console)
    CacherSélectionnez
  • newSingleThreadExecutor : même chose que précédemment avec une taille de 1, excepté que le service ne peut être reconfiguré (par exemple en transtypant).

    Executors.newSingleThreadExecutor - démo (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.executors.SingleThreadPoolDemo
    ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactoryLogger());
    
    Logger.println("Description du service:");
    afficherMembres(executor.getClass());
    
    Logger.println("");
    Random random = new Random();
    for (int i = 1; i <= 4; i++) {
      String nom = String.format("Tache-%02d", i);
      Runnable tache = new Runnable() {
        public void run() {
          int pause = random.nextInt(2_000);
          Logger.println("[%s] Pause %dms", nom, pause);
          TimeUnit.MILLISECONDS.sleep(pause);
          Logger.println("[%s] Fin", nom);
        }
      };
      executor.submit(tache);
    }
    executor.shutdown();
    
    Executors.newSingleThreadExecutor - démo (console)
    CacherSélectionnez

Sachez qu'il existe une interface fille de ExecutorService qui permet également la planification : ScheduledExecutorService. Ce type de service permet de retarder l'exécution d'une tâche et/ou de les exécuter de manière périodique.

III-C. Future

Un Future représente une tâche à exécuter et permet de récupérer son résultat via la méthode get() :

  • il peut s'agit de la valeur renvoyée :

    Future - Récupération du résultat (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureRécupérationRésultat
    Callable<Thread> tâche = new Callable<Thread>() {
      public Thread call() throws Exception {
        Logger.println("Début");
        Thread.sleep(2_000);
        Logger.println("Fin");
        return Thread.currentThread();
      }
    };
    ExecutorService executor = Executors.newSingleThreadExecutor();
    Future<Thread> future = executor.submit(tâche);
    Thread thread = future.get();
    Logger.println("La tâche a été exécutée sur %s", thread.getName());
    executor.shutdown();
    
    Future - Récupération du résultat (console)
    CacherSélectionnez
  • ou, il peut s'agir de l'exception levée :

    Future - Récupération de l'exception (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureRécupérationException
    class MonException extends Exception {}
    Callable<Thread> tâche = new Callable<Thread>() {
      public Thread call() throws Exception {
        Logger.println("Début");
        Logger.println("Fin");
        throw new MonException();
      }
    };
    ExecutorService executor = Executors.newSingleThreadExecutor();
    try {
      Future<Thread> future = executor.submit(tâche);
      Thread thread = future.get();
      Logger.println("La tâche a été exécutée sur %s", thread);
    } catch (ExecutionException e) {
      Logger.printStackTrace(e);
    } finally {
      executor.shutdown();
    }
    
    Future - Récupération de l'exception (console)
    CacherSélectionnez

Sachez qu'il est également possible d'attendre le résultat pendant un temps défini. Une TimeoutException est levée si l'attente expire.

La méthode cancel permet d'annuler une tâche qui a été soumise.

  • Si la tâche est dans la file d'attente, la tâche est simplement retirée de la file et n'est jamais exécutée.

    Future - Annulation lors de l'attente (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    41.
    42.
    43.
    44.
    45.
    46.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureAnnulationAttente
    Callable<Thread> tâche = new Callable<Thread>() {
      public Thread call() throws Exception {
        Logger.println("[Tâche] Début");
        Logger.println("[Tâche] Fin");
        return Thread.currentThread();
      }
    };
    Callable<Thread> attente = new Callable<Thread>() {
      public Thread call() throws Exception {
        Logger.println("[Attente] Début");
        Thread.sleep(3_000);
        Logger.println("[Attente] Fin");
        return Thread.currentThread();
      }
    };
    ExecutorService executor = Executors.newSingleThreadExecutor();
    
    // Monopolisation du service
    executor.submit(attente);
    
    // Soumission de la tâche à annuler
    Future<Thread> future = executor.submit(tâche);
    
    // Programme l'annulation
    new Thread("Annulation") {
      public void run() {
        try {
          Thread.sleep(2_000);
        } catch (InterruptedException e) {
          Logger.printStackTrace(e);
        } finally {
          Logger.println("Annulation de la tâche (%s)", future.cancel(false));
        }
      };
    }.start();
    
    // Récupération du résultat
    try {
      Thread thread = future.get();
      Logger.println("La tâche a été exécutée sur %s", thread);
    } catch (CancellationException e) {
      Logger.println("La tâche a été annulée");
    }
    
    executor.shutdown();
    
    Future - Annulation lors de l'attente (console)
    CacherSélectionnez
  • Si la tâche est déjà en cours d'exécution et qu'il n'y a pas de demande d'interruption, la tâche continue son exécution, mais il n'est plus possible de récupérer son résultat.

    Future - Annulation sans interruption (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureAnnulationSansInterruption
    Callable<Thread> tâche = new Callable<Thread>() {
      public Thread call() throws Exception {
        Logger.println("[Attente] Début");
        Thread.sleep(5_000);
        Logger.println("[Attente] Fin");
        return Thread.currentThread();
      }
    };
    ExecutorService executor = Executors.newSingleThreadExecutor();
    
    // Soumission de la tâche à annuler
    Future<Thread> future = executor.submit(tâche);
    
    // Programme l'annulation
    Thread.sleep(2_000);
    Logger.println("Annulation de la tâche (%s)", future.cancel(false));
    
    // Récupération du résultat
    try {
      Thread thread = future.get();
      Logger.println("La tâche a été exécutée sur %s", thread);
    } catch (CancellationException e) {
      Logger.println("La tâche a été annulée");
    }
    
    executor.shutdown();
    
    Future - Annulation sans interruption (console)
    CacherSélectionnez
  • S'il y a en plus une demande d'interruption, le thread exécutant la tâche est interrompu. D'une part, la tâche est libre de traiter cette interruption comme il lui convient ; d'autre part, le thread continuera à exécuter les tâches qui suivent.

    Future - Annulation avec interruption (code)
    Sélectionnez
    1.
    2.
    3.
    4.
    5.
    6.
    7.
    8.
    9.
    10.
    11.
    12.
    13.
    14.
    15.
    16.
    17.
    18.
    19.
    20.
    21.
    22.
    23.
    24.
    25.
    26.
    27.
    28.
    29.
    30.
    31.
    32.
    33.
    34.
    35.
    36.
    37.
    38.
    39.
    40.
    //com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.future.FutureAnnulationAvecInterruption
    Callable<Thread> tâche = new Callable<Thread>() {
      public Thread call() throws Exception {
        Logger.println("[Attente] Début");
        try {
          Thread.sleep(5_000);
        } catch (InterruptedException e) {
          Logger.println("[Attente] La tâche a été annulée");
        }
        Logger.println("[Attente] Fin");
        return Thread.currentThread();
      }
    };
    Callable<Thread> suite = new Callable<Thread>() {
      public Thread call() throws Exception {
        Logger.println("[Suite] Début");
        Logger.println("[Suite] Fin");
        return Thread.currentThread();
      }
    };
    ExecutorService executor = Executors.newSingleThreadExecutor();
    // Soumission des tâches
    Future<Thread> future1 = executor.submit(tâche);
    Future<Thread> future2 = executor.submit(suite);
    
    // Programme l'annulation
    Thread.sleep(2_000);
    Logger.println("Annulation de la tâche [Attente](%s)", future1.cancel(true));
    
    // Récupération des résultats
    try {
      Thread thread = future1.get();
      Logger.println("La tâche [Attente] a été exécutée sur %s", thread.getName());
    } catch (CancellationException e) {
      Logger.println("La tâche [Attente] a été annulée");
    }
    Thread thread = future2.get();
    Logger.println("La tâche [Suite  ] a été exécutée sur %s", thread.getName());
    
    executor.shutdown();
    
    Future - Annulation avec interruption (console)
    CacherSélectionnez

III-D. Fork/Join (1.7+)

Le framework Fork/Join est l'une des grandes nouveautés de Java 7. Il repose sur le principe « Diviser pour régner », c'est-à-dire découper le travail en petites tâches. Mais plutôt que découper le travail en tout petits morceaux que devront se disputer les threads, le principe est de diviser le traitement si la taille du problème est trop importante :

Fork/Join - principes
Sélectionnez
1.
2.
3.
4.
5.
6.
si (taille non critique) {
  <exécuter le traitement>
} sinon {
  <découper le traitement en deux parties>
  <lancer l'exécution des deux parties>
}

Avec l'API Fork/Join, cela donne un gabarit de cette forme :

Fork/Join - patron de code
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
abstract class MonAction extends java.util.concurrent.RecursiveAction {
  
  abstract boolean estTailleCritique();
  abstract void    avant();
  abstract void    traitement();
  abstract void    après();
  
  abstract java.util.concurrent.ForkJoinTask<Void> partie(int n);

  @Override
  protected void compute() {
    avant();

    if (!estTailleCritique()) {
      traitement();
    } else {
      invokeAll(partie(1), partie(2));
    }
    
    après();
  }
}

Concrètement, l'API se base sur la classe ForkJoinTask. Il est en revanche recommandé d'étendre RecursiveAction si votre traitement ne renvoie pas de résultat (void), sinon RecursiveTask.

Pour la démonstration, je vous propose un petit programme qui va simplement remplir un tableau d'entiers avec des nombres aléatoires :

Fork/Join - Démo (code)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinDemo
class Randomizer {
  int[] table = new int[1024 * 1024 * 256];
  void random(int start, int end) {
    ThreadLocalRandom random = ThreadLocalRandom.current();
    for (int i = start; i <= end; i++) {
      table[i] = random.nextInt();
    }
  }
  void randomAll() {
    random(0, table.length - 1);
    table = null;
  }
}
class RandomizerForkJoinAction extends RecursiveAction {
  Randomizer randomizer;
  int start, end;
  RandomizerForkJoinAction() {
    this.randomizer = new Randomizer();
    this.start      = 0;
    this.end        = randomizer.table.length - 1;
  }
  RandomizerForkJoinAction(Randomizer randomizer, int start, int end) {
    this.randomizer = randomizer;
    this.start      = start;
    this.end        = end;
  }

  @Override
  protected void compute() {
    if ((end - start) <= 1024 * 1024) {
      randomizer.random(start, end);
    } else {
      int mid = (this.end + this.start) /2;
      invokeAll(
          new RandomizerForkJoinAction(randomizer, start, mid),
          new RandomizerForkJoinAction(randomizer, mid+1, end)
      );
    }
  }
}

long start;

Logger.println("Monothread - début");
start = System.currentTimeMillis();
new Randomizer().randomAll();
Logger.println("Monothread - fin (%d ms)", System.currentTimeMillis() - start);

Logger.println("Fork/Join - début");
start = System.currentTimeMillis();
new ForkJoinPool().invoke(new RandomizerForkJoinAction());
Logger.println("Fork/Join - fin (%d ms)", System.currentTimeMillis() - start);
Fork/Join - Démo (console)
CacherSélectionnez

Mais pourquoi invoquer les sous-tâches alors que le thread courant va être en attente de leur résultat ? Et bien, c'est ce que va faire la méthode invokeAll ! Elle va mettre en file les tâches sauf une et l'exécuter, puis attendre que les sous-tâches se terminent :

Fork/Join - Tâche
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinTâche
abstract class ForkJoinTâche extends RecursiveAction {
  String  nom;
  ForkJoinTâche() {
    this.nom = getClass().getSimpleName();
  }
  ForkJoinTâche(int n) {
    this.nom = getClass().getSimpleName() + " " + n;
  }
  ForkJoinTâche(String nom) {
    this.nom = nom;
  }
  protected void compute() {
    Logger.println("[%s] Début", nom);
    calcul();
    Logger.println("[%s] Fin", nom);
  }
  abstract void calcul();
}
Fork/Join - Première tâche sur le même thread (code)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinMemeThread
class SousTâche extends ForkJoinTâche {
  void calcul() {
    TimeUnit.SECONDS.sleep(2);
  }
}
class SousTâche1 extends SousTâche {}
class SousTâche2 extends SousTâche {}
class Tâche extends ForkJoinTâche {
  void calcul() {
    invokeAll(new SousTâche1(), new SousTâche2());
  }
}

new ForkJoinPool().invoke(new Tâche());
Fork/Join - Première tâche sur le même thread (console)
Sélectionnez
1.
2.
3.
4.
5.
6.
00:00:00.020 [ForkJoinPool-1-worker-1] [Tâche] Début
00:00:00.028 [ForkJoinPool-1-worker-1] [SousTâche1] Début
00:00:00.028 [ForkJoinPool-1-worker-2] [SousTâche2] Début
00:00:02.028 [ForkJoinPool-1-worker-1] [SousTâche1] Fin
00:00:02.028 [ForkJoinPool-1-worker-2] [SousTâche2] Fin
00:00:02.029 [ForkJoinPool-1-worker-1] [Tâche] Fin
On remarque que Tâche et Sous-tâche 1 ont été exécutées par ForkJoinPool-1-worker-1 tandis que Sous-tâche 2 a été exécutée par ForkJoinPool-1-worker-2.

Mais que se passe-t-il s'il n'y a pas assez de threads pour exécuter les sous-tâches ? Ne risque-t-on pas d'avoir un deadlock ?

Fork/Join - Pas assez de thread (code)
Sélectionnez
1.
2.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinManqueThread
new ForkJoinPool(1).invoke(new Tâche());
Fork/Join - Pas assez de thread (console)
Sélectionnez
1.
2.
3.
4.
5.
6.
[ForkJoinPool-1-worker-1] Début de Tâche
[ForkJoinPool-1-worker-1] Début de Sous-tâche 1
[ForkJoinPool-1-worker-1] Fin   de Sous-tâche 1
[ForkJoinPool-1-worker-1] Début de Sous-tâche 2
[ForkJoinPool-1-worker-1] Fin   de Sous-tâche 2
[ForkJoinPool-1-worker-1] Fin   de Tâche
L'exemple est similaire au précédent, excepté que l'on a défini un niveau de parallélisme à 1. On remarque alors que les deux sous-tâches ont été exécutées par le thread qui était en train d'exécuter la tâche principale.

En revanche, il est possible de saturer votre pool si vous effectuez des tâches bloquantes :

Fork/Join - Tâches bloquées (code)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinBloqué
class Verrou {
  void prendre(); // Attend jusqu'à obtention verrou
  void libérer(); // Libère le verrou
}
Verrou verrou = new Verrou();
				
class Bloqueur extends ForkJoinTâche {
  void calcul() {
    Logger.println("[%s] Pose du verrou", nom);
    verrou.prendre();
    Logger.println("[%s] Calcul", nom);
    TimeUnit.SECONDS.sleep(5);
    verrou.libérer();
  }
}
class SousTâche extends ForkJoinTâche {
  SousTâche(int n) {
    super(n);
  }
  void calcul() {
    TimeUnit.SECONDS.sleep(1);
  }
}

class Tâche extends ForkJoinTâche {
  protected void calcul() {
    List<ForkJoinTask> actions = new ArrayList<>();
    actions.add(new Bloqueur());
    for (int i = 0; i < 5; i++) {
      actions.add(new SousTâche(i));
    }
    invokeAll(actions);
  }
}

Logger.println("Pose du verrou");
verrou.prendre();
Logger.println("Soumission");
Tâche tâche = new Tâche();
new ForkJoinPool(1).execute(tâche);
Logger.println("Pause");
TimeUnit.SECONDS.sleep(5);
Logger.println("Libération du verrou");
verrou.libérer();
tâche.join();
Fork/Join - Tâches bloquées (console)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
00:00:00.018 [main           ] Pose du verrou
00:00:00.026 [main           ] Soumission
00:00:00.029 [main           ] Pause
00:00:00.029 [ForkJoinPool-1-worker-1] [Tâche] Début
00:00:00.030 [ForkJoinPool-1-worker-1] [Bloqueur] Début
00:00:00.030 [ForkJoinPool-1-worker-1] [Bloqueur] Pose du verrou
00:00:05.029 [main           ] Libération du verrou
00:00:05.029 [ForkJoinPool-1-worker-1] [Bloqueur] Calcul
00:00:10.030 [ForkJoinPool-1-worker-1] [Bloqueur] Fin
00:00:10.030 [ForkJoinPool-1-worker-1] [SousTâche 0] Début
00:00:11.031 [ForkJoinPool-1-worker-1] [SousTâche 0] Fin
00:00:11.031 [ForkJoinPool-1-worker-1] [SousTâche 1] Début
00:00:12.032 [ForkJoinPool-1-worker-1] [SousTâche 1] Fin
00:00:12.032 [ForkJoinPool-1-worker-1] [SousTâche 2] Début
00:00:13.033 [ForkJoinPool-1-worker-1] [SousTâche 2] Fin
00:00:13.033 [ForkJoinPool-1-worker-1] [SousTâche 3] Début
00:00:14.034 [ForkJoinPool-1-worker-1] [SousTâche 3] Fin
00:00:14.034 [ForkJoinPool-1-worker-1] [SousTâche 4] Début
00:00:15.036 [ForkJoinPool-1-worker-1] [SousTâche 4] Fin
00:00:15.036 [ForkJoinPool-1-worker-1] [Tâche] Fin

Il est dommage qu'il ait fallu attendre que la tâche bloquante ait accès à la ressource pour exécuter les tâches qui n'en dépendaient pas.

Pour résoudre ce problème, Fork/Join framework propose l'interface ManagedBlocker. L'interface se compose de deux méthodes :

  • isReleasable : elle doit renvoyer vrai (true) lorsqu'il n'est pas/plus nécessaire de bloquer. Cela consiste généralement à essayer d'obtenir l'accès à la ressource de manière non bloquante ;
  • block : gère l'appel bloquant et renvoie vrai (true) si l'exécution peut se poursuivre.

Une fois l'instance créée, elle doit être passée en paramètre de la méthode statique ForkJoinPool.managedBlock. Retravaillons notre exemple :

Fork/Join - ManagedBlocker (code)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinManagedBlocker
class Verrou {
  // ...
  boolean tenter(); // Essaye de prendre le verrou et renvoie le résultat.
}

Verrou verrou = new Verrou();
class Verrouilleur implements ManagedBlocker, AutoCloseable {
  boolean verrouillé = false;
  Verrouilleur() {
    ForkJoinPool.managedBlock(this);
  }
  public boolean isReleasable() {
    if (!verrouillé) {
      verrouillé = verrou.tenter();
    }
    return verrouillé;
  }
  public boolean block() {
    if (!verrouillé) {
      verrou.prendre();
      verrouillé = true;
    }
    return verrouillé;
  }
  public void close() {
    verrou.libérer();
    verrouillé = false;
  }
}

class Bloqueur extends ForkJoinTâche {
  void calcul() {
    Logger.println("[%s] Pose du verrou", nom);
    try (Verrouilleur verrouilleur = new Verrouilleur()) {
      Logger.println("[%s] Calcul", nom);
      TimeUnit.SECONDS.sleep(2);
    }
  }
}
class SousTâche extends ForkJoinTâche {
  SousTâche(int n) {
    super(n);
  }
  void calcul() {
    TimeUnit.SECONDS.sleep(1);
  }
}
class Tâche extends ForkJoinTâche {
  protected void calcul() {
    List<ForkJoinTask> actions = new ArrayList<>();
    actions.add(new Bloqueur());
    for (int i = 0; i < 10; i++) {
      actions.add(new SousTâche(i));
    }
    invokeAll(actions);
  }
}

Logger.println("Pose du verrou");
verrou.prendre();
Logger.println("Soumission");
Tâche tâche = new Tâche();
new ForkJoinPool(1).execute(tâche);
Logger.println("Pause");
TimeUnit.SECONDS.sleep(3);
Logger.println("Libération du verrou");
verrou.libérer();
tâche.join();
Fork/Join - ManagedBlocker (console - Java 8)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
00:00:00.027 [main           ] Pose du verrou
00:00:00.037 [main           ] Soumission
00:00:00.040 [main           ] Pause
00:00:00.041 [ForkJoinPool-1-worker-1] [Tâche] Début
00:00:00.043 [ForkJoinPool-1-worker-1] [Bloqueur] Début
00:00:00.043 [ForkJoinPool-1-worker-1] [Bloqueur] Pose du verrou
00:00:00.047 [ForkJoinPool-1-worker-0] [SousTâche 9] Début
00:00:01.047 [ForkJoinPool-1-worker-0] [SousTâche 9] Fin
00:00:01.047 [ForkJoinPool-1-worker-0] [SousTâche 8] Début
00:00:02.048 [ForkJoinPool-1-worker-0] [SousTâche 8] Fin
00:00:02.048 [ForkJoinPool-1-worker-0] [SousTâche 7] Début
00:00:03.041 [main           ] Libération du verrou
00:00:03.041 [ForkJoinPool-1-worker-1] [Bloqueur] Calcul
00:00:03.049 [ForkJoinPool-1-worker-0] [SousTâche 7] Fin
00:00:03.049 [ForkJoinPool-1-worker-0] [SousTâche 6] Début
00:00:04.050 [ForkJoinPool-1-worker-0] [SousTâche 6] Fin
00:00:04.050 [ForkJoinPool-1-worker-0] [SousTâche 5] Début
00:00:05.043 [ForkJoinPool-1-worker-1] [Bloqueur] Fin
00:00:05.043 [ForkJoinPool-1-worker-1] [SousTâche 0] Début
00:00:05.052 [ForkJoinPool-1-worker-0] [SousTâche 5] Fin
00:00:05.052 [ForkJoinPool-1-worker-0] [SousTâche 4] Début
00:00:06.044 [ForkJoinPool-1-worker-1] [SousTâche 0] Fin
00:00:06.044 [ForkJoinPool-1-worker-1] [SousTâche 1] Début
00:00:06.053 [ForkJoinPool-1-worker-0] [SousTâche 4] Fin
00:00:06.053 [ForkJoinPool-1-worker-0] [SousTâche 3] Début
00:00:07.045 [ForkJoinPool-1-worker-1] [SousTâche 1] Fin
00:00:07.045 [ForkJoinPool-1-worker-1] [SousTâche 2] Début
00:00:07.053 [ForkJoinPool-1-worker-0] [SousTâche 3] Fin
00:00:08.046 [ForkJoinPool-1-worker-1] [SousTâche 2] Fin
00:00:08.046 [ForkJoinPool-1-worker-1] [Tâche] Fin

On remarque alors deux choses. La première, c'est que le pool a alloué un nouveau thread pour traiter les tâches.

La deuxième chose, c'est que les sous-tâches n'ont pas été exécutées selon l'ordre de soumission à cause du (grâce au ?) work-stealing algorithm. Le deuxième thread a pris les tâches selon leur ordre inverse pour limiter l'utilisation de verrou pour accéder à la liste de tâches. En utilisant les deux extrémités de la liste des tâches, on limite les contentions.

Il peut-être intéressant de savoir que Phaser (1.7+) et l'API CompletableFuture (1.8+) exploite ce mécanisme pour limiter les contentions.

Il est important de noter que le comportement avec une JVM 1.7 n'est pas exactement le même. En effet, dans la version précédente, le nouveau thread ne traitait les tâches que le temps du blocage. Comme le montrent les traces ci-dessous.

Fork/Join - ManagedBlocker (console - Java 7)
CacherSélectionnez

Cependant, il est important de savoir que l'utilisation de ManagedBlocker est surtout dédiée à limiter les deadlocks. En effet, chaque worker bloqué n'est pas remplacé. Des workers supplémentaires sont uniquement alloués si le pool le permet (limite du nombre de threads) et si TOUS les workers sont bloqués :

Fork/Join - Limite des ManagedBlockers (code)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
//com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.forkjoin.ForkJoinManagedBlockerLimite
class TâcheBloquante extends ForkJoinTâche {
  Verrou verrou1 = new Verrou();
  Verrou verrou2 = new Verrou();
  TâcheBloquante(int n) {
    super(n);
  }
  protected void calcul() {
    Logger.println("[%s] Attente non-managed", nom);
    verrou1.prendre();

    Logger.println("[%s] Passage en mode 'managed'", nom);
    try (Verrouilleur verrouilleur = new Verrouilleur(verrou2)) {}
  }
}
class SousTâche extends ForkJoinTâche {
  SousTâche(int n) {
    super(n);
  }
  @Override
  void calcul() {
    TimeUnit.SECONDS.sleep(1);
  }
}
class Tâche extends ForkJoinTâche {
  @Override
  void calcul() {
    List<ForkJoinTâche> soustâches = new ArrayList<>();
    for (int i = 1; i <= 5; i++) {
      soustâches.add(new SousTâche(i));
    }
    invokeAll(soustâches);
  }
}

int parallélisme = 2;
ForkJoinPool pool = new ForkJoinPool(parallélisme);

Logger.println("Saturation du pool");
List<TâcheBloquante> bloqués = new ArrayList<>(parallélisme);
for (int i = 1; i <= parallélisme; i++) {
  TâcheBloquante bloqué = new TâcheBloquante(i);
  bloqué.verrou1.prendre();
  bloqué.verrou2.prendre();
  pool.submit(bloqué);
  bloqués.add(bloqué);
}

Logger.println("Ajout de tâches supplémentaires");
Tâche tâche = new Tâche();
pool.submit(tâche);

for (TâcheBloquante bloqué : bloqués) {
  TimeUnit.SECONDS.sleep(5);
  bloqué.verrou1.libérer();
}

tâche.join();
Logger.println("Libération des tâches bloquantes");
for (TâcheBloquante bloqué : bloqués) {
  bloqué.verrou2.libérer();
  bloqué.join();
}
Fork/Join - Limite des ManagedBlockers (console)
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
00:00:00.020 [main           ] Saturation du pool
00:00:00.031 [main           ] Ajout de tâches supplémentaires
00:00:00.031 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Début
00:00:00.031 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Attente non-managed
00:00:00.031 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Début
00:00:00.032 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Attente non-managed
00:00:05.032 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Passage en mode 'managed'
00:00:10.032 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Passage en mode 'managed'
00:00:10.033 [ForkJoinPool-1-worker-2] [Tâche] Début
00:00:10.034 [ForkJoinPool-1-worker-2] [SousTâche 1] Début
00:00:11.035 [ForkJoinPool-1-worker-2] [SousTâche 1] Fin
00:00:11.035 [ForkJoinPool-1-worker-2] [SousTâche 2] Début
00:00:12.036 [ForkJoinPool-1-worker-2] [SousTâche 2] Fin
00:00:12.036 [ForkJoinPool-1-worker-2] [SousTâche 3] Début
00:00:13.037 [ForkJoinPool-1-worker-2] [SousTâche 3] Fin
00:00:13.037 [ForkJoinPool-1-worker-2] [SousTâche 4] Début
00:00:14.038 [ForkJoinPool-1-worker-2] [SousTâche 4] Fin
00:00:14.038 [ForkJoinPool-1-worker-2] [SousTâche 5] Début
00:00:15.039 [ForkJoinPool-1-worker-2] [SousTâche 5] Fin
00:00:15.039 [ForkJoinPool-1-worker-2] [Tâche] Fin
00:00:15.040 [main           ] Libération des tâches bloquantes
00:00:15.040 [ForkJoinPool-1-worker-1] [TâcheBloquante 1] Fin
00:00:15.041 [ForkJoinPool-1-worker-0] [TâcheBloquante 2] Fin
On a commencé par saturer le pool avec des tâches bloquantes, puis on a ajouté une tâche qui produisait cinq sous-tâches. On constate alors que rien ne se produit.
Ensuite, on passe peu à peu les tâches bloquantes en mode « managed ». Tant qu'elles ne sont pas toutes passées en mode « managed », il ne se passe toujours rien.
Dès que la dernière bloquante est passée en mode « managed », le pool alloue uniquement un troisième worker qui traite la tâche (et toutes ses filles).
Finalement on libère les tâches bloquantes.

Si vous devez utiliser des opérations bloquantes (verrous ou non), il est recommandé de s'orienter vers des API asynchrones.

IV. Conclusion

Au cours de ce premier article, nous avons vu comment sont représentés les threads en Java et comment interagir avec eux. Ceci pose les fondations de la programmation parallèle, mais les prochains articles seront entièrement dédiés à gérer la concurrence dans votre code à l'aide de l'API native (intégrée au langage) et des outils fournis par Java SE.

V. Remerciements

Je remercie tous les contributeurs de l'API Java et les packages java.util.concurrent.* et plus particulièrement Doug Lea qui est l'un des principaux auteurs.

Je remercie également Mickael Baron, Thierry Leriche-Dessirier alias thierryler et Claude Leloup pour leur relecture attentive, leurs remarques et leurs bons conseils.

Je tiens aussi à remercier la communauté Developpez.com qui a mis en place tous les outils, procédures et l'hébergement nécessaires à la publication de cet article.

Enfin mon épouse et mes enfants pour leur patience et leur tolérance durant les nombreuses heures qui ont été nécessaires à la rédaction de cet article.

VI. Annexes

VI-A. Sources des exemples

Tous les exemples donnés dans cet article sont disponibles sous la forme d'un projet Maven hébergé sous GitHub. Tous les exemples cités contiennent une première ligne commentaire indiquant l'emplacement du fichier dans les sources. Les sources propres à ce chapitre se trouve sous le package com.developpez.lmauzaize.java.concurrence.ch01_gestion_threads.

Si vous ne savez pas comment importer le projet, je vous invite à consulter l'article « Importer un projet Maven dans Eclipse en 5 minutes ».

VI-B. Java Concurrent Animated

Java Concurrent Animated est un projet Swing visant à montrer graphiquement le comportement de différents composants de l'API concurrente de Java.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2014 Logan Mauzaize. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.