Adloun

Threads

Cours · programmation Java (école d'ingénieur), chapitre 15 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

Un navigateur télécharge une page pendant que vous faites défiler la précédente ; un traitement de texte vérifie l'orthographe pendant que vous tapez ; un serveur répond à des centaines de clients à la fois. Tous ces programmes font plusieurs choses en même temps. En Java, chacune de ces activités est un thread, ou fil d'exécution. Ce chapitre montre comment en créer, comment les attendre, et surtout comment éviter les pièges redoutables qui apparaissent dès que plusieurs fils partagent des données.

À faire sur votre ordinateur

L'Éditeur Java de la plateforme exécute les programmes dans le navigateur, sur un seul fil : les programmes de ce chapitre demandent un JDK installé sur votre ordinateur. Leurs sorties ne sont d'ailleurs pas toujours les mêmes d'une exécution à l'autre : c'est l'une des leçons du chapitre. Les sorties reproduites ici sont celles d'une exécution réelle.

Objectifs du chapitre

  • Distinguer processus et thread, et dire pourquoi on programme en parallèle.
  • Créer et démarrer un thread (Thread, Runnable, lambda), l'attendre avec join.
  • Comprendre que l'ordre d'exécution n'est pas prévisible, et reconnaître une condition de course.
  • Protéger les données partagées avec synchronized et les classes atomiques.
  • Reconnaître et éviter un interblocage.
  • Utiliser les outils de haut niveau : file bloquante, pool de fils, Callable et Future.

15.1 Processus et threads

Définition 15.1Processus, thread

Un processus est un programme en cours d'exécution : il possède sa propre mémoire, isolée de celle des autres processus. Un thread (fil d'exécution) est une suite d'instructions exécutée à l'intérieur d'un processus. Un processus peut contenir plusieurs threads : ils s'exécutent en même temps et partagent la mémoire du processus (les objets du tas), mais chacun a sa propre pile d'appels et ses propres variables locales.

Tout programme Java a au moins un fil : celui qui exécute main, et qui s'appelle justement main. La JVM en lance d'autres en coulisses (par exemple pour le ramasse-miettes).

Pourquoi plusieurs fils ?

15.2 Créer et démarrer un thread

Un thread est un objet de la classe java.lang.Thread. Le code qu'il exécute est celui de sa méthode run(). Il y a trois façons de le fournir.


/** Chapitre 15 — trois façons de créer un thread. */
public class TroisFacons {
    // 1) Hériter de Thread et redéfinir run()
    static class Salueur extends Thread {
        private final String texte;
        Salueur(String texte) { this.texte = texte; }
        @Override public void run() {
            for (int i = 0; i < 3; i++) System.out.println(texte + " " + i);
        }
    }

    // 2) Implémenter Runnable et confier l'objet à un Thread
    static class Compteur implements Runnable {
        @Override public void run() {
            for (int i = 0; i < 3; i++) System.out.println("Runnable " + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Salueur("Hérité");
        Thread b = new Thread(new Compteur());
        // 3) Donner directement le code sous forme de lambda
        Thread c = new Thread(() -> {
            for (int i = 0; i < 3; i++) System.out.println("Lambda " + i);
        });
        a.start(); b.start(); c.start();      // les trois fils démarrent
        a.join(); b.join(); c.join();         // main attend leur fin
        System.out.println("Fin du main");
    }
}

Une exécution :


Lambda 0
Lambda 1
Lambda 2
Runnable 0
Runnable 1
Runnable 2
Hérité 0
Hérité 1
Hérité 2
Fin du main

Les fils ont été démarrés dans l'ordre a, b, c, et pourtant c'est c qui a fini le premier : rien ne garantit l'ordre. Seule certitude ici : « Fin du main » vient en dernier, grâce aux join.

Proposition 15.2start, run et join
  • t.start() crée un nouveau fil d'exécution, qui exécute t.run(), et rend la main immédiatement : l'appelant continue en parallèle. On ne peut démarrer un thread qu'une fois.
  • t.join() bloque le fil appelant jusqu'à la fin de t. Cette méthode peut lever une exception contrôlée, de type InterruptedException.
  • Le programme s'arrête quand tous ses fils (sauf les fils démons) sont terminés, même si main a fini avant eux.

Méthode — quelle façon choisir ?

Préférer Runnable ou une lambda à l'héritage de Thread : on sépare ainsi la tâche (ce qu'il faut faire) du moyen de l'exécuter (un thread, un pool de threads, section 15.7), et la classe reste libre d'hériter d'une autre classe.

15.2.1 L'erreur classique : run au lieu de start


/** Chapitre 15 — l'erreur classique : appeler run() au lieu de start(). */
public class StartOuRun {
    public static void main(String[] args) throws InterruptedException {
        Runnable qui = () -> System.out.println("exécuté par : " + Thread.currentThread().getName());
        Thread t = new Thread(qui, "Travailleur");
        t.run();      // appel de méthode ordinaire : c'est main qui l'exécute !
        t.start();    // nouveau fil d'exécution
        t.join();
    }
}

exécuté par : main
exécuté par : Travailleur

Attention

Appeler t.run() n'est qu'un appel de méthode : le code s'exécute dans le fil courant, sans aucun parallélisme, et le programme « marche » quand même. C'est pourquoi l'erreur passe souvent inaperçue. Pour démarrer un thread, c'est start(), toujours.

15.2.2 Attendre : sleep et join

Thread.sleep(ms) suspend le fil courant pendant le nombre de millisecondes indiqué, sans consommer de temps de calcul. Le programme suivant simule trois tâches d'une seconde, d'abord à la suite, puis en parallèle.


/** Chapitre 15 — sleep et join : trois tâches de 1 s en parallèle prennent 1 s, pas 3. */
public class Attente {
    static void tache(String nom) {
        try {
            Thread.sleep(1000);                       // simule un travail d'une seconde
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(nom + " terminée");
    }

    public static void main(String[] args) throws InterruptedException {
        long debut = System.currentTimeMillis();
        tache("séquentielle 1"); tache("séquentielle 2"); tache("séquentielle 3");
        System.out.printf("En séquence : %.1f s%n", (System.currentTimeMillis() - debut) / 1000.0);

        debut = System.currentTimeMillis();
        Thread[] fils = new Thread[3];
        for (int i = 0; i < 3; i++) {
            String nom = "parallèle " + (i + 1);
            fils[i] = new Thread(() -> tache(nom));
            fils[i].start();
        }
        for (Thread t : fils) t.join();
        System.out.printf("En parallèle : %.1f s%n", (System.currentTimeMillis() - debut) / 1000.0);
    }
}

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →