2. Lançons nos premiers threads

2.1. Introduction

Avant de se lancer dans la notion d'état, de pose de verrous, toutes ces choses qui font les délices de la complexité de la programmation concurrente, penchons-nous sur des problèmes beaucoup plus terre à terre : comment lancer des threads en Java. Il y a deux types de réponses à cette question, suivant que l'on décide d'utiliser ou non l'API Concurrent, introduite en Java 5. En fait, ne pas l'utiliser dans une application réelle est sans aucun doute une erreur de conception, peut-être imputable au fait que l'on ignore son existence. Cette API, construite sur les concepts de programmation multithreadée des versions précédentes de Java, ne les remet pas en cause, et ne les modifie pas, elle les enrichit. Il ne s'agit pas d'une API développée à côté de la précédente, et qui permet d'oublier la classe Thread ou l'interface Runnable et leurs fonctionnements, mais d'un enrichissement, qui étend ces anciennes fonctionnalités, tout en les conservant. Il faut donc examiner la première pour comprendre le fonctionnement de la seconde. Écrivons donc un premier thread , à l'ancienne, comme on le faisait au bon vieux temps de Java 1.1. Un thread en Java est un objet, qui doit étendre la classe Thread. Comme Java ne supporte pas l'héritage multiple, il n'est pas toujours possible d'étendre cette classe. Java nous propose donc aussi un mécanisme d'enveloppe ( wrapper ), que nous allons voir.

2.2. Un premier thread, extension de Thread

Écrivons un premier thread par extension de la classe Thread (package java.lang). Nous devons surcharger une seule méthode : la méthode run(), qui retourne void, et ne prend aucun paramètre. Cette méthode ne jette non plus aucune exception, et nous verrons que cela pose en fait un problème.

Exemple 54. Premier thread par extension de la classe Thread

public  class Bateau  extends Thread {

    // surcharge de la méthode run() de la classe Thread
    public  void run() {
      
       int n =  0 ; 
       while (n++ <  100) {
         System.out.println("Je vogue !") ;
          try {
      
            Thread.sleep(10) ;
         }  catch (InterruptedException e) {
         
             // gestion de l'erreur
         }
      }
   }
}

Lancer un thread signifie demander à la machine Java d'exécuter cette classe dans un fil d'exécution particulier. Par convention, le code exécuté dans ce fil est le code écrit dans la méthode run() de ce thread. Voyons comment lancer ce thread.

Exemple 55. Lancement d'un premier thread

// dans une méthode main()
 // instanciantion d'un objet de type Thread
Bateau b =  new Bateau() ;

 // lancement de ce thread par appel à sa méthode start()
b.start() ;

 // cette méthode rend immédiatement la main...
System.out.println("Thread lancé") ;

 // ... mais la méthode main() ne quitte pas immédiatement

Le lancement d'un nouveau thread se fait par appel à la méthode start() de l'instance de Thread que l'on veut exécuter. Notons ici qu'il s'agit bien de la méthode start(), et non pas de la méthode run(), que l'on a surchargée. Appeler la méthode run() au lieu de start() permet d'exécuter le code de ce thread , mais dans le fil d'exécution courant. Aucun nouveau fil n'est créé dans ce cas, ce n'est en général pas ce que l'on veut ! Appeler run() à la place de start() est une erreur que l'on rencontre parfois. L'appel à cette méthode start() a pour effet de créer un nouveau fil d'exécution dans la machine Java. Ce fil d'exécution va exécuter la méthode run() de cet objet. C'est pourquoi l'on surcharge cette méthode. Dans le cas de notre exemple, l'appel à la méthode b.start() rend la main immédiatement. On voit donc s'afficher dans la console, immédiatement, le message Thread lancé. Contrairement à l'exécution d'un programme classique, la méthode main() ne quitte pas une fois son exécution terminée. Une fois toutes les 10 millisecondes, on voit le message Je vogue ! s'afficher. Une fois qu'il s'est affiché 100 fois, alors notre application exemple se termine. Que se passe-t-il lorsque l'on appelle par erreur la méthode run() plutôt que la méthode start() ? La méthode run() va s'exécuter, mais dans le thread courant, puisque seul l'appel à start() déclenche la création d'un nouveau thread au niveau de la machine Java. On pourra avoir l'impression que tout se passe normalement, puisque les traitements seront tout de même effectués. Cela dit, comme tout se passe dans le même fil d'exécution, l'ordre des affichages n'est pas le même. Les messages Je vogue ! s'affichent tout d'abord. Suit enfin le message Thread lancé, prometteur, mais erroné ! D'une façon générale, une application qui lance des threads ne peut pas quitter normalement tant que tous ces threads sont encore actifs. Il faut donc les arrêter, nous verrons comment. Un type de thread particulier, appelé daemon ne présente pas cette contrainte.

2.3. Un deuxième thread, implémentation de Runnable

La première façon de lancer un thread est la plus simple, mais elle n'est en général pas utilisable, du fait qu'une classe Java ne peut pas étendre deux classes à la fois. Il existe donc une deuxième façon de faire, qui commence par implémenter l'interface Runnable. Cette interface n'expose qu'une unique méthode : run().

Exemple 56. Deuxième thread, par implémentation de Runnable

public  class AutreBateau  implements Runnable {
   
    // implémentation de la méthode run() de l'interface Runnable
    public  void run() {
      
       int n =  0 ; 
       while (n++ <  100) {
         System.out.println("Je vogue aussi !") ;
          try {
      
            Thread.sleep(10) ;
         }  catch (InterruptedException e) {
         
             // gestion de l'erreur
         }
      }
   }
}

Cette deuxième classe ressemble énormément à la première, c'est en fait la même à un détail près : elle implémente Runnable au lieu d'étendre Thread. La façon dont on lance un tel thread , est en revanche fondamentalement différente. Une instance de cette classe doit être passée en paramètre du constructeur d'une nouvelle instance de Thread, et c'est sur ce nouveau thread que nous allons invoquer la méthode start().

Exemple 57. Lancement d'un deuxième thread

// dans une méthode main()
 // instanciantion d'un objet de type Runnable
AutreBateau autreBateau =  new AutreBateau() ;

 // construction d'un Thread en passant cette instance de Runnable en paramètre
Thread thread =  new Thread(autreBateau) ;

 // lancement de ce thread par appel à sa méthode start()
thread.start() ;

 // cette méthode rend immédiatement la main...
System.out.println("Thread lancé") ;

 // ... mais la méthode main() ne quitte immédiatement

L'objet de type Thread que l'on crée ici est complètement générique. Appeler sa méthode start() demande toujours à la machine Java de créer un nouveau fil d'exécution, et le fonctionnement interne de la classe Thread fait que ce fil va exécuter la méthode run() de l'objet passé en paramètre lors de la construction de notre instance de Thread.

2.4. Remarque sur la méthode Thread.sleep(long)

Nous avons utilisé la méthode Thread.sleep(long) dans nos deux exemples, sans expliquer son fonctionnement. Le long passé en paramètre correspond au nombre de millisecondes que cette méthode doit attendre pour rendre la main. Elle jette une exception de type InterruptedException, que l'on doit gérer de manière standard. Ici nous avons choisi de capter cette exception. Il existe également une méthode wait(long) sur la classe Object. Elle fait la même chose, mais son fonctionnement interne est très différent. On évitera de l'utiliser, et on lui préfèrera la méthode Thread.sleep(long). Nous verrons dans la suite pourquoi cette exception est jetée.

2.5. Arrêter un thread

Arrêter un thread n'est pas une chose si simple. En tout cas, il y a une chose qu'il ne faut pas surtout pas faire pour stopper un thread , c'est appeler sa méthode stop(). Cette méthode est dépréciée, et ne doit plus être utilisée. On ne peut pas, dans le cadre de l'API Concurrent, arrêter un thread de l'extérieur, de façon autoritaire. La bonne façon d'arrêter un thread , est de positionner un champ particulier de ce thread . Il est alors de la responsabilité du thread de vérifier périodiquement la valeur de ce champ, et lorsqu'elle lui indique qu'il faut qu'il s'interrompe, c'est à lui de le faire. Cette approche est valide dans la plupart des cas, mais pas dans le cas où le thread qui doit s'arrêter, est lui-même en attente du retour d'une opération bloquante, ou dans un état d'attente (méthodes Object.wait(long millis) ou Thread.sleep(long millis). Dans ce cas, on peut interrompre le thread . Ces méthodes d'attente sont en général capables de détecter cette interruption, et jettent alors une exception de type InterruptedException.

Exemple 58. Interruption d'un thread

// dans une méthode main
Runnable r =  new Runnable() {

    public  void run() {
   
       try {
         
          while (!Thread.currentThread().isInterrupted()){
         
             // traitements
            ...
            
             // petite sieste
            Thread.sleep(5000) ;
         }
      
      }  catch (InterruptedException e) {
         
          // nous avons été interrompu
          // on remet interrupted à false par l'appel à cette méthode
         Thread.currentThread().interrupted() ;
      }
   }
   
    public  void cancel() {
      
       // interruption du thread courant, c'est-à-dire le nôtre
      Thread.currentThread().interrupt() ;
   }
}

Le thread de l'exemple précédent peut être interrompu dans tous les cas par appel à sa méthode cancel(). Notons le comportement d'un thread interrompu.
  1. positionnement à true de la propriété interrupted ;
  2. si le thread est dans une opération bloquante, qui supporte l'interruption, alors cette opération remet interrupted à false, et jette une exception de type InterruptedException ;
  3. si ce n'est pas le cas, interrupted est laissé à true. Sa valeur peut être testée par appel à Thread.currentThread().isInterrupted(). Il est de la responsabilité du traitement de s'interrompre alors.
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