Blog ENI : Toute la veille numérique !
🎁 Jusqu'au 31/12, recevez notre
offre d'abonnement à la Bibliothèque Numérique. Cliquez ici
🎁 Jusqu'au 31/12, recevez notre
offre d'abonnement à la Bibliothèque Numérique. Cliquez ici
  1. Livres et vidéos
  2. Apprendre la Programmation Orientée Objet avec le langage Java
  3. Le multithreading
Extrait - Apprendre la Programmation Orientée Objet avec le langage Java (avec exercices pratiques et corrigés) (4e édition)
Extraits du livre
Apprendre la Programmation Orientée Objet avec le langage Java (avec exercices pratiques et corrigés) (4e édition) Revenir à la page d'achat du livre

Le multithreading

Introduction

La programmation multithread est un domaine passionnant mais qui peut devenir rapidement très complexe à mettre au point. Plusieurs exécutions parallèles au cœur de votre application devront partager des informations, s’attendre, échanger... Le succès d’une architecture de ce type repose avant tout sur une analyse solide. Ce chapitre n’a pas vocation de vous exposer toutes les possibilités de programmation multithread et leurs implémentations en Java mais de vous en présenter l’essentiel avec la philosophie POO.

Comprendre le multithreading

Un processus peut effectuer des traitements longs qui vont bloquer l’application durant leurs exécutions. Pour éviter cela, le développeur peut créer une sorte de chemin d’exécution parallèle qui va prendre en charge ce traitement et ainsi délester l’exécuteur principal. Dans ce cas, le système d’exploitation Windows de Microsoft partage très rapidement le temps machine entre les différents flux d’exécution (typiquement quelques ms par tranche de temps) donnant l’illusion d’une exécution simultanée. On parle de système d’exploitation préemptif. Le contenu d’une file d’exécution peut enchaîner tous les traitements qu’elle souhaite sans se soucier du temps que cela prendra globalement.

Le système d’exploitation viendra « l’interrompre » périodiquement pour donner du temps à la file d’exécution suivante et ainsi de suite jusqu’à revenir sur elle pour qu’elle reprenne son traitement là où il avait été interrompu.

Reprenons pour exemple cette carte d’acquisition d’entrées/sorties équipée d’une interface de programmation très sommaire. Le constructeur nous livre un jeu de fonctions permettant, entre autres...

Multithreading et Java

La partie encapsulation de processus est proposée au travers de la classe java.lang.Process. Comme la création d’un processus est étroitement liée au système d’exploitation dans lequel fonctionne la machine virtuelle, il faut passer par une classe très spécialisée qui s’appelle Runtime. Chaque application Java dispose de façon native d’une instance unique sur un objet de type Runtime et c’est grâce à elle que vous pourrez démarrer un nouveau processus.

L’extrait de code suivant (figurant dans le répertoire Chap8\demo_process du .zip accompagnant cet ouvrage) permet d’exécuter le programme Windows calc.exe depuis un programme Java.

import java.io.IOException; 
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or 
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter. 
public class Main { 
    public static void main(String[] args) { 
// Récupération d'une référence sur le "runtime" 
        Runtime runtime = Runtime.getRuntime(); 
        try { 
            // Utilisation de sa méthode exec ...

Implémentation des threads en Java

Il existe deux façons principales de programmer des threads en Java : étendre la classe Thread ou implémenter l’interface Runnable.

1. Étendre la classe Thread

En étendant la classe Thread et en plaçant le code à dérouler dans la méthode run, reprise à votre compte, votre classe devient directement « threadable ». Après son instanciation, un simple appel à la méthode start permet de démarrer le thread et d’exécuter en parallèle le code contenu dans sa méthode run. Le thread s’arrête dès que le code de la méthode run a été entièrement déroulé ou qu’une exception non gérée a été levée. Pratique, non ?

Voici un exemple de code (figurant dans le répertoire Chap8\demo_thread du .zip accompagnant cet ouvrage) utilisant ce principe :

// MaClasseThread étend la classe java.lang.Thread 
public class MaClasseThread extends java.lang.Thread { 
 
    //... 
    //... On imagine ici différentes méthodes et accesseurs 
    //... 
 
    // On place dans la méthode run le traitement "long" 
    // qui va être exécuté dans un thread instancié 
    // et démarré depuis le code appelant  
    // (le main dans cet exemple) 
    @Override 
    public void run(){ 
 
        // Trace de début de traitement. 
        // On utilise la méthode de type static 
        // Thread.currentThread() 
        // pour afficher le nom attribué à ce thread 
        System.out.println("Début d'un traitement de 10 sec " 
                + "dans le thread " 
                + Thread.currentThread().getName()); 
 
        // Ici on simule un travail de 10 secondes (10 x 1000 ms) 
        for(int i=0; i<10; i++) ...

Synchronisation entre threads

1. Nécessité de la synchronisation

La programmation de plusieurs chemins d’exécution ne pose aucun problème particulier jusqu’à ce qu’ils partagent les mêmes informations ou les mêmes ressources... En effet, étant donné que le système d’exploitation peut interrompre les traitements à n’importe quel moment, il risque d’y avoir des objets en cours de modification dans un thread préempté qui se retrouveront dans des états instables pour le thread suivant. Pour se prémunir de ces dysfonctionnements, il faut synchroniser les threads, c’est-à-dire protéger des zones délicates de traitements.

Cela ne va pas jouer sur le système de gestion qui continuera à activer les threads les uns après les autres ; simplement lorsqu’un thread A aura besoin d’accéder à une donnée commune protégée qu’un thread B n’aura pas terminé de mettre à jour, alors le thread A devra « attendre le prochain tour ». Et si au prochain tour le travail du thread B n’est toujours pas terminé alors il devra attendre le suivant, etc.

Même principe s’il s’agit d’un traitement commun que le thread B devra avoir terminé avant que le thread A ne puisse l’effectuer à son tour. C’est ce scénario que l’extrait de code suivant propose. En effet, le traitement permet d’afficher un comptage de un jusqu’à vingt effectué par dix threads. L’objectif à atteindre est l’affichage suivant :

1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 
1234567891011121314151617181920 

Et voici une première version de code sans protection (figurant dans le répertoire Chap8\demo_thread_sans_synchro du .zip accompagnant cet ouvrage) :

public class TraitementSansProtection{ 
 
    public void ExecuterLeTraitement() { 
 
       ...

Communication inter-threads

1. La méthode join

Comme vu précédemment, la méthode join permet à un thread principal de « s’endormir » en attendant la fin de l’exécution d’un thread secondaire.

Exemple de code figurant dans le répertoire Chap8\demo_synchro_inter_ threads_avec_join du .zip accompagnant cet ouvrage

public class Main { 
 
    public static void main(String[] args) 
            throws InterruptedException { 
 
        System.out.println("Instanciation " 
                + "du thread secondaire"); 
        Thread monThreadSecondaire 
                = new Thread(new MaClasseRunnable()); 
 
        System.out.println("Démarrage " 
                + "du thread secondaire"); 
        monThreadSecondaire.start(); 
 
        // Simule un travail de 3 sec. sur le thread principal 
        Thread.sleep(3000); 
 
        System.out.println("Demande " 
                + "l'arrêt du thread secondaire"); 
        monThreadSecondaire.interrupt(); 
 
        System.out.println("Attend " 
                + "la fin du thread secondaire"); 
        monThreadSecondaire.join(); 
 
        System.out.println("Fin du main"); 
    } 
 
} 

 

public class MaClasseRunnable implements Runnable { ...

Exercice

1. Énoncé

En partant de l’exemple précédent (avec Thread.sleep(500); dans la boucle de consommation), vous devez introduire la notion de gestion de flux entre producteur et consommateur. Le thread de production doit « s’endormir » quand un nombre maximum de trames en attente d’être traitées est atteint (dix par exemple). Le thread de consommation viendra lire ces trames puis, quand la file sera vide, le thread de production devra reprendre son travail.

Type de comportement souhaité :

debug: 
Début Traitement global 
Début ProducteurDeTrames 
Appuyez sur Enter pour arrêter... 
Début ConsommateurDeTrames 
Consommateur attend 
Trame reçue : 46 
Trame consommée : 46 
Trame reçue : 67 
Trame reçue : 23 
Trame reçue : 92 
Trame reçue : 20 
Trame reçue : 23 
Trame consommée : 67 
Trame reçue : 54 
Trame reçue : 21 
Trame reçue : 60 
Trame reçue : 66 
Trame consommée : 23 
Trame reçue : 2 
Trame reçue : 58 
 
Trame reçue : 69 
 
File saturée 
Trame consommée : 92 
Trame consommée : 20 
Trame consommée : 23 
Trame consommée : 54 
Trame consommée : 21 
Trame consommée : 60 
Trame consommée : 66 
Trame consommée : 2 
Trame consommée : 58 
Trame consommée : 69 
File dé-saturée 
Trame reçue : 97 
Trame reçue : 76 
Trame reçue : 41 
Trame reçue : 89 
Trame consommée : 97 
Trame reçue : 12 
 
Abandon demandé 
InterruptedException dans ProducteurDeTrames 
Fin ProducteurDeTrames 
InterruptedException dans ConsommateurDeTrames 
Fin ConsommateurDeTrames 
Fin Traitement global 
BUILD SUCCESSFUL (total time: 9 seconds) 

Voici quelques informations pour vous aider :

Dans le code de départ, le développeur a fait jouer un double rôle au producteur : produire et synchroniser le consommateur pour qu’il s’endorme quand il n’y a rien à lire... Une solution possible à la nouvelle problématique serait...