Passer au contenu principal

De nombreuses applications ont recours aux threads. Ceux-ci permettent notamment de lancer des opérations asynchrones, ou tout simplement d’éviter de bloquer la GUI. Seulement, la facilité de lancer un thread fait qu’on est tenté d’appeler directement new Thread() dès qu’on veut en lancer un. Or il s’agit d’une erreur pouvant avoir de fâcheuses conséquences sur les performances de l’application pour plusieurs raisons :

  • Lancer un nouveau thread coûte très cher en terme de CPU. Rien que la commutation volontaire d’un thread vers un autre coûte 80000 cycles CPU, car il faut passer d’une pile à l’autre, vider certains registres, etc, etc.
  • Si on n’y prête pas garde, le nombre de threads lancés par votre application peut dépasser le nombre de coeurs d’exécution disponible sur la machine sur laquelle l’appli est déployée. Et là c’est catastrophique car le processeur passera alors plus de temps à effectuer des commutations de threads qu’à exécuter votre code.

Dès lors, voyons quelques bonnes pratiques pour l’usage des threads.

Implémentation d’une classe devant tourner dans un thread

Quand on implémente une classe devant tourner dans un thread, il est tentant d’hériter directement de java.lang.Thread. Or il s’agit là d’une erreur de design, car si votre classe a besoin d’hériter d’une autre classe, vous ne pouvez pas le faire. Le mieux est que votre classe implémente directement l’interface java.lang.Runnable, qui contient directement la méthode public void run() appelée par java.lang.Thread.

Ensuite, pour lancer le thread, il convient d’appeler la méthode start() et non la méthode run(), cette dernière n’exécutant pas de thread mais exécutant simplement le code de votre méthode run().

Trucs et astuces sur l’implémentation des threads

Si vous lisez la doc des threads, vous verrez qu’il y a quelques méthodes qu’il ne faut en aucun cas appeler :

  • suspend()
  • resume()
  • stop()

En fait c’est tout simplement parce qu’elles ne sont pas considérées comme sûres. Dès lors voici une petite implémentation permettant d’avoir tout de même ces fonctionnalités. Comme vous pouvez le constater, tout n’est pas idéal non plus car le stop et le suspend n’ont pas d’effet immédiat. D’un autre côté on évite ainsi d’interrompre un traitement en cours brutalement, ce qui permet de ne pas avoir d’état indéfini lorsqu’on suspend l’exécution de notre thread.


public class MyRunnable implements Runnable {

   /**
    * The lock monitor, used to manage suspend/resume.
    */
   private final Object lockMonitor = new Object();

   /**
    * Flag indicating that the thread must be stopped. Note that it is volatile to ensure that all threads
    * always take the last value written into account.
    */
   private volatile boolean isStop;

   /**
    * Flag indicating that the thread must be suspended. Note that it is volatile to ensure that all threads
    * always take the last value written into account.
    */
   private volatile boolean isSuspend;

   /**
    * Constructor.
    */
   public MyRunnable() {
      this.isStop = false;
      this.isSuspend = false;
   }

   /**
    * Stop the thread.
    */
   public synchronized void stop() {
      this.isStop = true;
      lockMonitor.notifyAll();
   }

   /**
    * Suspend the thread.
    */
   public synchronized void suspend() {
      this.isSuspend = true;
   }

   /**
    * Resume the thread.
    */
   public synchronized void resume() {
      this.isSuspend = false;
      lockMonitor.notifyAll();
   }

   /**
    * {@inheritDoc}
    */
   public void run() {
      while (!this.isStop) {

         synchronized(lockMonitor) {
             while (this.isSuspend) {
               try {
                  lockMonitor.wait();
               } catch (InterruptedException e) {
                  // Log exception
               }
               if (this.isStop) {
                  return;
               }
            }
         }

         // Do your stuff
      }
   }
}

Gestion manuelle des pools de threads

Pour gérer manuellement un pool de threads, le plus simple à faire est d’avoir d’un côté votre implémentation de java.lang.Runnable qui contient un traitement, et de l’autre côté une BlockingQueue qui contient la liste des tâches à exécuter, sur laquelle on aura bien pris soin de définir une taille.

L’idée est en fait qu’un ou plusieurs threads vont produire des descripteurs de tâches, et un ou plusieurs threads vont les traiter. La blocking queue va mettre les producteurs en attente quand elle sera pleine, et les consommateurs en attente quand elle sera vide.

Le dernier truc à faire est d’ajouter à tout ce beau monde un flag permettant de savoir quand tout doit être arrêté.

J’ai mis ici un exemple de code pour la gestion manuelle d’un pool de threads.

L’ExecutorService

Le rôle de l’ExecutorService, apparu avec Java 5, est de permettre de gérer plus simplement un pool de threads. L’idée est qu’au départ on dimensionne l’ExecutorService pour un certain nombre de threads, et ensuite on lui passe des Callable our des Runnable à exécuter de manière asynchronie.

Un bon article (en anglais) présente de manière plus détaillée l’ExecutorService.

Et alors, que choisir entre une gestion manuelle des pools de threads et l’ExecutorService ?

Il est vrai que la gestion manuelle de pools de threads demande d’écrire un peu plus de code, par contre son point fort par rapport à un ExecutorService auquel on soumettrait en peu de temps un grand nombre de tâches est de permettre d’éviter de saturer la mémoire. L’ExecutorService utilise aussi une BlockingQueue de Callable et Runnable pour obtenir le même résultat, mais sans qu’on ait directement la main dessus, bref après c’est selon les goûts et les couleurs.

Cet article vous a plu ? Vous aimerez sûrement aussi :

Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.

Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.

Son Twitter Son LinkedIn

Rejoignez la discussion 2 Commentaires

  • Gérard Turmel dit :

    Il y a un leger bug dans le code. Il faudrait tester le cas non stop dans le test du while:
    while (isSuspend && ! isStop) {
    ….
    }
    Sinon pour finir un thread suspendu il faudrait commencer par le relancer.

    Et puis vous synchronisez les méthodes stop, resume sur l’objet this et sur lockMonitor dans le while.
    Je préfère synchroniser les accès concurrents sur un objet avec un unique monitor toujours private final.

  • gojul dit :

    Bonjour,

    Merci pour votre retour.

    Pour le isStop -> aucun souci du fait que le while() parent contient déjà le isStop. Du fait que la variable est en volatile il n’y a aucun souci à ce niveau (au pire on aura une itération supplémentaire de la boucle).

    Le notify() sert à relancer le thread (notamment quand on fait le stop pour sortir de l’état suspend, mais aussi le resume).

    Les boolean sont en volatile, ce qui signifie que chaque thread est garanti de lire la dernière valeur écrite même en dehors de toute valeur synchronized, d’où le fait que le lockMonitor n’est pas utilisé pour les méthodes stop(), suspend() et resume().

    Pour le lockMonitor, pour le coup je n’utiliserais pas le même lock pour le synchronized sur les méthodes suspend(), resume() et stop() que le lockMonitor. Effectivement ça peut être un autre objet, mais utiliser l’objet qui sert justement à endormir le thread et le réveiller pour la synchronisation est dangereux.
    Les méthodes suspend(), resume() et stop() sont synchronized simplement pour garantir qu’un thread qui a invoqué resume() ne va pas le faire pendant que suspend() est invoqué.

Laisser un commentaire