9. Pattern executor

9.1. Notion de réserve de threads

Dans la partie précédente, nous avons vu deux façons de lancer un thread :
  • en étendant la classe Thread, et en invoquant la méthode start() de cet objet ;
  • en implémentant l'interface Runnable, en passant cet objet en tant que paramètre de construction d'une instance de Thread, et en invoquant la méthode start() de cet objet Thread.
Dans la pratique, ces deux méthodes permettent de faire des exercices, mais ne sont pas réellement utilisables. Effectivement, le nombre de threads que l'on peut ouvrir est en général limité, soit par les ressources de calcul, soit par les ressources en mémoire de la machine sur laquelle on se trouve. Si l'on conçoit des applications multithreadées , c'est pour optimiser les temps d'exécution. Typiquement, une tâche qui accède à de nombreuses ressources réseau ou disque, risque de passer du temps à attendre que ces ressources soient effectivement disponibles. Lancer ces tâches dans des threads donne une chance à des tâches qui ne travaillent qu'en mémoire de s'exécuter plus rapidement. Cela dit, la gestion d'un grand nombre de threads entraîne également une surcharge pour la machine Java. Si ce nombre de threads devient trop important, il se peut que cette surcharge pénalise les performances de l'ensemble de l'application. Il y a donc un équilibre à trouver entre un nombre de threads suffisant pour fluidifier une application, et pas trop important pour ne pas effondrer les performances de la machine Java. D'une façon générale, il n'est pas viable de laisser une application ouvrir autant de threads qu'elle le veut, sans aucun contrôle. On utilise donc une notion supplémentaire, celle de thread pools . Ces réserves de threads gèrent un nombre en général fixé et borné de threads. Lorsque l'on a une tâche à lancer dans un nouveau thread , on la soumet à cette réserve, qui la prend en charge, et la fait exécuter par un thread disponible. La création des threads ainsi que la gestion de leur cycle de vie est entièrement à la charge de cette réserve.

9.2. Interface Executor

La première interface définie par ce pattern est l'interface Executor. Une implémentation de cette interface prend en charge une implémentation de Runnable, et a pour objet de l'exécuter dans un nouveau Thread, à un moment ou à un autre. Cette interface est très simple et ne comporte qu'une seule méthode.

Exemple 83. Interface Executor

package java.util.concurrent ;

 public  interface Executor {

    public  void execute(Runnable runnable) ;
}

On peut imaginer de nombreuses implémentations de cette classe, de la plus simple à la plus compliquée. L'API Java nous en fournit une, que nous allons voir dans la suite.

9.3. Interface ExecutorService

La deuxième interface de ce pattern est l'interface ExecutorService. Cette deuxième interface est plus complexe. Elle expose des méthodes destinées à gérer un service d'exécution, et donc probablement dans la pratique, une réserve de threads . L'interface ExecutorService étend Executor. Cette interface définit la notion de cycle de vie pour un service d'exécution. Lorsque qu'il est créé, ce service est dans l'état RUNNING. Lorsque l'on veut l'éteindre, on le passe dans l'état SHUTTING_DOWN par appel à sa méthode shutdown(). Dans cet état, il ne peut plus accepter de nouvelles tâches, et le service tente d'éteindre les tâches qu'il est en train d'exécuter. Si l'on veut l'éteindre de façon urgente, alors on fait appel à sa méthode shutdownNow(), qui va tout arrêter sans attendre la fin normale de l'exécution des tâches en cours.

9.4. Interface ScheduledExecutorService

La troisième interface de ce pattern est l'interface ScheduledExecutorService. Cette troisième interface permet de lancer des tâches périodiques. Ces tâches peuvent se lancer après un certain laps de temps, et être répétées dans le temps à intervalles réguliers. Voyons ses principales méthodes.
  • schedule(Callable<V> command, long delay, TimeUnit unit) : crée une tâche qui ne se lancera qu'une seule fois, après le délai indiqué par le paramètre delay.
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) : lance la tâche passée en paramètre à intervalles de temps réguliers, indépendamment du temps que cette tâche met à s'exécuter.
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) : lance cette tâche, puis attend le délai initialDelay avant de la lancer à nouveau, de façon périodique. On remarquera que cette méthode ne fonctionne pas comme la précédente.
Ces trois méthodes retournent des objets de type ScheduledFuture<V>.

9.5. Classe Executors

La classe Executors est une classe utilitaire qui expose un jeu de méthodes statiques permettant de gérer l'ensemble des opérations multithreadées que nous avons présentées. Les méthodes qu'elle propose peuvent se ranger dans les catégories suivantes.

9.5.1. Création de réserves de type ExecutorService

La classe Executors permet de créer des réserves de threads de différentes façons. Chaque méthode crée sa réserve suivant une certaine sémantique.
  • newCachedThreadPool() : crée une réserve capable de créer des nouveaux threads à la demande, sans limite. Lorsqu'un thread n'est plus utilisé, cette réserve est capable de lui confier de nouvelles tâches. Lorsqu'une nouvelle tâche est confiée à cet executor, elle est lancée immédiatement.
  • newFixedThreadPool(int nThreads) : crée le même type de réserve que la méthode précédente. La différence est que le nombre maximal de threads est fixé à la construction de cet objet. Dans ce cas, il est devient possible qu'une tâche confiée à cette réserve ne puisse pas être exécutée immédiatement. Cette réserve gère donc une file d'attente dans laquelle ces tâches sont placées. Cette file d'attente ne peut pas saturer.
  • newSingleThreadExecutor() : crée le même type de réserve que la méthode précédente. Cette fois, la réserve de threads ne comporte qu'un unique thread . Les tâches qui ne peuvent être immédiatement exécutées sont également placées dans une file d'attente.

9.5.2. Création de réserves de type ScheduledExecutorService

Les méthodes suivantes permettent de créer des instances de ScheduledExecutorService
  • newScheduledThreadPool(int corePoolSize) : crée une réserve dont le nombre de threads est égal à corePoolSize.
  • newSingleThreadScheduledExecutor() : crée une réserve de même type que la méthode précédente, fonctionnant avec un unique thread .

9.6. Pattern de lancement de tâches

Voyons un exemple de lancements de tâches dans des threads , en utilisant le pattern Executors.

Exemple 84. Lancement de tâches - pattern Executors

// dans une méthode main()
 // création d'un service d'exécution, doté de 4 threads
ExecutorService service = Executors.newFixedThreadPool(4) ;

 // création de 20 futures pour récupérer le résultat 
 // de nos tâches
Future<Integer> [] futures =  new Future[20] ;

 for (int i =  0 ; i < futures.length ; i++) {
    // pour être utilisée dans une classe anonyme, 
    // cette variable doit être final, ce ne peut donc pas être i
    final  int k = i ;
   System.out.println("Lancement de " + k) ;
   futures[i] = service.submit(new Callable<Integer>() {

       public Integer call()  throws Exception {
          // simulation d'un traitement
         System.out.println("[" + k +  "] thread : " + Thread.currentThread().getName()) ;
          return k ;
      }

   }) ;
}

 // autres traitement dans le thread courant

 // tableau des résultats
 int [] result =  new  int [futures.length] ;

 // récupération des résultats par interrogation des
 // futures
 for (int i =  0 ; i < futures.length ; i++) {
    try {
       // on donne 100ms à un future pour répondre
      result[i] = futures[i].get(100, TimeUnit.MILLISECONDS) ;
      System.out.println(result[i]) ;
   
    // gestion des exceptions
   }  catch (TimeoutException e) {
      result[i] = -1 ;
   }  catch (ExecutionException e) {
      result[i] = -1 ;
   }  catch (InterruptedException e) {
      result[i] = -1 ;
   }
}

 // fermeture du service
 // sans quoi la JVM reste active
service.shutdown() ;
System.out.println("Fin du traitement") ;

Si l'on exécute cette application, on se rend bien compte que nos tâches sont bien exécutées dans quatre threads différents, et pas plus.
Java API avancées
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
API Collection
1. Introduction
2. Interface Collection
2.1. Notion de Collection
2.2. Détail des méthodes disponibles
2.3. Interface Iterator
2.4. Implémentation, exemples d'utilisation
3. Interface List
3.1. Notion de List
3.2. Détail des méthodes disponibles
3.3. Interface ListIterator
3.4. Implémentations, exemples d'utilisation
4. Interface Set
4.1. Notion de Set
4.2. Implémentations HashSet et LinkedHashSet
4.3. Exemples d'utilisation
5. Interface SortedSet
5.1. Notion de SortedSet
5.2. Détails des méthodes disponibles
5.3. Exemples d'utilisation
6. Interface NavigableSet
6.1. Notion de NavigableSet
6.2. Détails des méthodes disponibles
6.3. Exemple d'utilisation
7. Interfaces Queue et Deque
7.1. Notion de file d'attente
7.2. Détail des méthodes disponibles
7.3. Utilisation des interfaces Queue et Deque
8. Tables de hachage
8.1. Notion de table de hachage
8.2. Interface Map
8.3. Interface Map.Entry
8.4. Interface SortedMap
8.5. Interface NavigableMap
8.6. Implémentations
8.7. Exemples d'utilisation
9. Classes utilitaires Collections et Arrays
9.1. Introduction
9.2. Classe Arrays
9.3. Classe Collections
Génériques
1. Introduction
2. Un premier exemple
2.1. Une première classe générique
2.2. Une première méthode générique
3. Contraindre un type générique
3.1. Problème posé
3.2. Contraindre un type générique
4. Implémentation des génériques
4.1. Type erasure
4.2. Types génériques et casts
4.3. Type générique et exception
4.4. Construction d'une instance générique
4.5. Génériques et membres statiques
4.6. Collisions de méthodes génériques
4.7. Implémentation de plusieurs types identiques
5. Type <?>
5.1. Introduction
5.2. Type ? extension d'un type
5.3. Type ? super-type d'un type
Expressions régulières
1. Introduction
2. Mise en œuvre des expressions régulières
2.1. Fonctionnement d'une regexp
2.2. Fonctionnement de l'API en Java
2.3. Un premier exemple
2.4. Classe Pattern
2.5. Classe Matcher
2.6. Utilisation des méthode find() et group()
2.7. Méthodes de remplacement
2.8. Sélection de régions
3. Syntaxe des expressions régulières
3.1. Notion de classe
3.2. Étude d'un cas réel
3.3. Recherche d'un mot précis
3.4. Recherche de deux mots précis
3.5. Recherche d'un mot commençant par une lettre donnée
3.6. Cas de mots comportant des caractères accentués
3.7. Recherche sur les lignes
Introspection
1. Introduction
2. La classe Class
2.1. Utilisation de Class
2.2. Méthodes disponibles
2.3. Remarque sur la propriété Accessible
2.4. Type d'une classe
2.5. Création d'une instance à partir d'un objet Class
2.6. Cas des énumérations
3. Les classes Method et Constructor
3.1. Utilisation de Method
3.2. Utilisation de Constructor
3.3. Méthodes disponibles
3.4. Invocation d'une méthode par introspection
4. La classe Field
4.1. Utilisation de Field
4.2. Méthodes disponibles
4.3. Accès à un champ par introspection
5. La classe Modifier
Programmation concurrente
1. Introduction
2. Lançons nos premiers threads
2.1. Introduction
2.2. Un premier thread, extension de Thread
2.3. Un deuxième thread, implémentation de Runnable
2.4. Remarque sur la méthode Thread.sleep(long)
2.5. Arrêter un thread
3. Concurrence d'accès
3.1. Notion d'état
3.2. Exemple de concurrence d'accès sur un état
3.3. Analyse de la concurrence d'accès
3.4. Solution au problème
3.5. Champs volatile
4. Synchronisation
4.1. Définition d'un bloc synchronisé
4.2. Fonctionnement d'un bloc synchronisé
4.3. Notion de deadlock
4.4. Bonnes pratiques pour la synchronisation de threads
5. Opérations atomiques
5.1. Atomicité d'une opération
5.2. Solutions disponibles
5.3. Variables atomiques
6. Collections synchronisées et concurrentes
6.1. Introduction
6.2. Position du problème
6.3. Solutions proposées
7. Files d'attente
7.1. Introduction, pattern producteur / consommateur
7.2. Interface BlockingQueue<E>
7.3. Implémentations de BlockingQueue
7.4. Exemple de producteur / consommateur
7.5. Arrêter un producteur / consommateur : pilule empoisonnée
8. Classes utilitaires de l'API Concurrent
8.1. Introduction
8.2. Énumération TimeUnit
8.3. Interface Callable<V>
8.4. Interfaces Future<V> et RunnableFuture<V>
8.5. Interface ScheduledFuture<V> et RunnableScheduledFuture<V>
9. Pattern executor
9.1. Notion de réserve de threads
9.2. Interface Executor
9.3. Interface ExecutorService
9.4. Interface ScheduledExecutorService
9.5. Classe Executors
9.6. Pattern de lancement de tâches
10. Classes de contrôle d'accès
10.1. Introduction
10.2. Interfaces Lock et ReadWriteLock
10.3. Notion de verrou réentrant
10.4. Classe RentrantLock
10.5. Classe ReadWriteRentrantLock
11. Sémaphores, barrières et latches
11.1. Introduction
11.2. Notion de sémaphore, classe Semaphore
11.3. Notion de latch, classe CountDownLatch
11.4. Notion de barrière, classe CyclicBarrier