Bien fermer ses threads en Java

Mis à jour le jeudi 31 octobre 2013

Sur ce, bonjour à tous.

Dans cet article, nous allons traiter de la fermeture des threads. Si vous avez déjà programmé avec des threads, vous avez probablement été confrontés au problème de leur arrêt.

Certains, peu scrupuleux, auront tout simplement utilisé la méthode Thread.stop (pourtant dépréciée), tandis que d'autres auront fait des recherches pour essayer de les fermer de la façon la plus propre possible.

Nous allons donc voir pourquoi il ne faut pas utiliser cette fameuse méthode Thread.stop , et ce qu'il faut faire à la place.

Mais pourquoi la méthode Thread.stop est-elle dépréciée ?

Tout d'abord, il faut déjà comprendre ce que veut dire déprécié. Si nous passons rapidement par le dictionnaire du CNRTL, nous trouvons la définition suivante :

Citation : CNRTL

  • faire baisser la valeur de (quelque chose), diminuer sa valeur ;

  • rabaisser la valeur de (quelque chose), porter des jugements défavorables sur ;

  • Rabaisser (la valeur ou le mérite de) ; porter ou exprimer un jugement défavorable sur.

Ce que nous retiendrons pour notre usage informatique, est qu'une méthode dépréciée est à éviter. Si elle est dépréciée, c'est qu'il existe une alternative pour faire la même chose ou mieux.

Maintenant, passons aux raisons de la dépréciation. Ce n'est pas parce que la méthode est moins optimisée qu'une autre, mais bien parce qu'elle est dangereuse d'utilisation et induit des comportements aléatoires.

Pour expliciter cela, regardons rapidement la documentation :

Citation : Javadoc

Deprecated. This method is inherently unsafe. Stopping a thread with Thread.stop causes it to unlock all of the monitors that it has locked (as a natural consequence of the unchecked ThreadDeath exception propagating up the stack). If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to other threads, potentially resulting in arbitrary behavior. Many uses of stop should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), the interrupt method should be used to interrupt the wait. For more information, seeWhy are Thread.stop, Thread.suspend and Thread.resume Deprecated?.

En résumé, cette méthode n'est pas sûre. L'utiliser pour arrêter un thread provoque le déverrouillage de toutes les ressources utilisées par le dit thread. Si une de ces ressources n'était pas dans un état stable, elle va être rendue accessible dans son état instable, ayant pour effet un comportement incontrôlable et aléatoire lors de l'usage de la ressource par d'autres threads. La documentation explique ensuite une méthode simple et générique d'arrêt « propre » de vos threads, et vous renvoie vers un lien si vous voulez en savoir plus sur le pourquoi du comment.

Pour mieux comprendre le problème, rien ne vaut un exemple.

Imaginons deux threads qui accèdent à un objet « Personne », l'un pour écrire, l'autre pour lire.
Le code est bien fait, les deux threads ne peuvent accéder à l'objet en même temps. Maintenant, imaginons que l'objet « Personne » réponde à un instant « t » aux valeurs suivantes : {Prénom = Sophie, Nom = Dupond, âge = 23 ans, taille = 170 cm, salaire = 32000 ? / an}. Le premier thread va pour le modifier en {Jean, Durant, 2 ans, 65 cm, 0 ? / an}. Il commence sa besogne, mais au milieu de son travail, il se fait arrêter (par Thread.stop ). Il se ferme et le verrou se libère. Or, il se trouve que le deuxième thread était justement en attente de la libération de ce verrou pour lire l'objet. Il va donc pour effectuer sa lecture, mais lit {Jean, Durant, 3 ans, 170 cm, 32000 ? / an} parce que le premier thread n'a pas eu le temps de finir son écriture avant d'être fermé. Les valeurs que le deuxième thread récupère sont erronées et surtout inexploitables ! Elles ne correspondent à personne. Il va donc travailler avec de « mauvaises » valeurs, produisant bien entendu un résultat incohérent.

On ne peut prédire ce que le premier thread aura modifié dans l'objet lors de son arrêt. C'est en ce sens que le comportement du reste du programme va devenir aléatoire. On n'a aucune idée de l'état dans lequel seront les données.

Une alternative, la boucle conditionnelle

Maintenant que nous savons comment il ne faut pas faire, voyons comment il faut faire.
Cette première solution que je vais vous présenter suffit dans une grande partie des cas. Elle est simple à mettre en œuvre et ne nécessite que très peu de code.

Prenons pour l'exemple un thread standard codé pour être arrêté avec Thread.stop :

class MonThread extends Thread {
    public Monthread () {
        // Mon constructeur
    }

    @Override
    public void run() {
        // Initialisation
        while(true) { // Boucle infinie pour effectuer des traitements.
            // Traitement à faire
        }
    }
}

Il suffit juste de changer et de mettre une condition dans la boucle while(running) avec running un booléen, et le tour est joué.

Pour changer la valeur de running, deux possibilités :

  • mettre une condition dans la boucle infinie ;

  • écrire une méthode.

Nous obtenons finalement le code suivant :

class MonThread extends Thread {
    protected volatile boolean running = true;
    public Monthread () {
        // Mon constructeur
    }

    public arret() { // Méthode 2
        running = false;
    }

    @Override
    public void run() {
        // Initialisation
        while(running) { // Boucle infinie pour effectuer des traitements.
            // Traitement à faire
            if (/*Condition d'arret*/) // Méthode 1
                running = false;
        }
    }
}

Pour un code bloquant, l'utilisation des interruptions

Comme on peut vite s'en rendre compte, l'utilisation du code précédent ne fonctionne que si le thread boucle régulièrement (puisqu'il ne remarquera la demande d'arrêt qu'au commencement de la boucle). Imaginons maintenant que le thread fasse de longues attentes (comme un Thread.sleep de 5 minutes, un wait , un Thread.join ou autre), il faudrait alors attendre que l'attente de ce dernier se termine avant qu'il ne se ferme. Cette option n'est bien entendu pas envisageable, c'est pourquoi il existe un mécanisme d'interruption des threads lorsqu'ils sont en attente. Ce mécanisme répond à l'appel de la méthode Thread.interrupt.

Passons à la pratique, car son utilisation est quelque peu délicate. Prenons pour exemple le code suivant :

public class MonThread extends Thread {

    @Override
    public void run() {
        while(true) {
            // Traitement
            try {
                Thread.sleep(100000); // Pause de 100 secondes
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt(); // Très important de réinterrompre
                break; // Sortie de la boucle infinie
            }
        }
    }

    public static void main(String[] args)
            throws InterruptedException {
        Thread t = new MonThread();
        t.start();
        Thread.sleep(1000); // Attente 1 seconde avant d'interrompre
        t.interrupt();
    }
}

Lorsque le main appelle t.interrupt();, cela interrompt l'attente du thread « t » et lève l'exception InterruptedException. Il faut donc faire un catch de cette exception et la traiter comme la condition de sortie de boucle (c'est pour ça que l'on fait un break;).

Le seul morceau de code qu'il reste à expliquer est maintenant la ligne Thread.currentThread().interrupt();.

Mais pourquoi faut-il que le thread s'interrompe lui-même ?

Encore une fois, prenons un exemple :

public class MonThread extends Thread {
    private static boolean correct = true;

    @Override
    public void run() {
        while (true) {
            // Traitement 1
            for (int i = 0; i < 10; i++) { // Boucle imbriquée
                // Traitement 2
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                    if (correct)
                        Thread.currentThread().interrupt(); // réinterruption sur soi-même
                    System.out.println("premier Catch");
                    break; // Sortir de la boucle <italique>for</italique>
                }
            }
            try {
                // Traitement 3
                System.out.print("Devant le sleep, ");
                Thread.sleep(1000);
                System.out.print("Derrière le sleep, ");
            } catch (InterruptedException ex) {
                if (correct)
                    Thread.currentThread().interrupt();
                System.out.println("Deuxième catch");
                break; // Sortir de la boucle <italique>while</italique>
            }
        }
        System.out.println("Fermeture du Thread");
    }

    private static void test() throws InterruptedException {
        Thread t = new MonThread();
        t.start();
        Thread.sleep(2500);
        t.interrupt();
        t.join(); // Attente de la fermeture du <italique>thread</italique> avant de continuer.
        System.out.println("Fin du Thread"); // Le <italique>thread</italique> a été fermé.
    }

    public static void main(String[] args)
            throws InterruptedException {
        test();
        correct = false; // Désactive la réinterruption du <italique>thread</italique> sur lui-même.
        test();
    }
}

Si vous exécutez ce code, vous verrez qu'il ne s'arrête jamais (il ne sort pas de la boucle infinie), et on obtient la sortie suivante :

Citation : Résultat

Devant le sleep, Derrière le sleep, Premier catch
Devant le sleep, Deuxième catch
Fermeture du Thread
Fin du Thread
Devant le sleep, Derrière le sleep, Premier catch
Devant le sleep, Derrière le sleep, Devant le sleep, Derrière le sleep, Devant le sleep, Derrière le sleep, ...

On constate que si l'on relance l'interruption, celle-ci est détectée par le sleep de la boucle infinie englobante, qui donc passe dans le catch pour fermer le thread proprement. En revanche, si l'interruption n'est pas relancée, elle n'atteint pas le sleep la boucle englobante qui donc continue son exécution normalement.

L'exception des InputStream

Bien sûr, tout ne pouvait être si beau et si simple, il fallait que ça ne fonctionne pas quelque part. C'est sur les lectures de flux que nous allons avoir un problème. Si votre thread est bloqué sur une ES, l'interruption ne fonctionnera pas. Java a prévu, pour pallier ce problème, une autre exception : InterruptedIOException qui hérite de IOException . Malheureusement, cette exception n'est pas encore bien gérée et ne fonctionne pas sur toutes les ES.

Le meilleur (seul) moyen pour sortir de cette attente devient donc la fermeture du flux bloquant (j'admets que ce n'est pas génial). Heureusement, la méthode Thread.interrupt n'est pas final , il est donc possible de surcharger la méthode pour fermer le flux dans l'interruption.

Voici un code en exemple :

import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class MonThread extends Thread {
    private InputStream in;

    public MonThread(InputStream in) {
        this.in = in;
    }

    @Override
    public void interrupt() {
        super.interrupt();
        try {
            in.close(); // Fermeture du flux si l'interruption n'a pas fonctionné.
        } catch (IOException e) {}
    }

    @Override
    public void run() {
        try {
            System.out.println("Avant la lecture");
            in.read();
            System.out.println("Après la lecture");
        } catch (InterruptedIOException e) { // Si l'interruption a été gérée correctement.
            Thread.currentThread().interrupt();
            System.out.println("Interrompu via InterruptedIOException");
        } catch (IOException e) {
            if (!isInterrupted()) { // Si l'exception ne vient pas d'une interruption.
                e.printStackTrace();
            } else { // <italique>Thread</italique> interrompu mais <italique>InterruptedIOException</italique> n'était pas gérée pour ce type de flux.
                System.out.println("Interrompu");
            }
        }
    }

    public static void test1() // Test avec un socket
            throws IOException, InterruptedException {
        ServerSocket ss = new ServerSocket(4444); // Pour accepter la connexion <italique>socket</italique>.
        Socket socket = new Socket("localhost", 4444);
        Thread t = new MonThread(socket.getInputStream());
        t.start();
        Thread.sleep(1000);
        t.interrupt();
        t.join();
    }

    public static void test2() // Test avec un <italique>PipedOutputStream</italique>
            throws IOException, InterruptedException { 
        PipedInputStream in = new PipedInputStream(new PipedOutputStream());
        Thread t = new MonThread(in);
        t.start();
        Thread.sleep(1000);
        t.interrupt();
        t.join();
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        test1();
        test2();
    }
}

Citation : Résultat

Avant la lecture
Interrompu
Avant la lecture
Interrompu via InterruptedIOException

Étant donné que InterruptedIOException n'est pas gérée pour tous les flux, il faut s'assurer de la fermeture du thread avec les deux méthodes présentées. Avec les deux tests, on voit d'ailleurs que, contrairement au pipedOutputStream , les InterruptedIOException ne sont pas gérées pour les sockets.

Et voilà, j'espère que vous n'aurez plus de problèmes pour arrêter vos threads maintenant. En espérant vous avoir aidés, je vous souhaite une bonne continuation dans votre apprentissage du Java.

déroulement d'un cours

  • 1

    Dès aujourd'hui, vous avez accès au contenu pédagogique et aux exercices du cours.

  • 2

    Vous progressez dans le cours semaine par semaine. Une partie du cours correspond à une semaine de travail de votre part.

  • !

    Les exercices doivent être réalisés en une semaine. La date limite vous sera annoncée au démarrage de chaque nouvelle partie. Les exercices sont indispensables pour obtenir votre certification.

  • 3

    À l'issue du cours, vous recevrez vos résultats par e-mail. Votre certificat de réussite vous sera également transmis si vous êtes membre Premium et que vous avez au moins 70% de bonnes réponses.

Exemple de certificat de réussite
Exemple de certificat de réussite