Développons en Java
v 2.40   Copyright (C) 1999-2023 .   
37. Les threads 39. Le framework Executor Imprimer Index Index avec sommaire Télécharger le PDF

 

38. L'association de données à des threads

 

chapitre    3 8

 

Niveau : niveau 4 Supérieur 

 

Dans un environnement monothread, il suffit de déclarer une variable static pour stocker une valeur contextuelle.

Dans un contexte multithread, il est possible de stocker des données dans le contexte d'exécution de chaque thread. Ainsi chaque thread peut avoir sa propre instance d'une donnée. Ceci évite :

Il existe plusieurs solutions pour permettre d'associer des données à un thread :

Il est possible de définir sa propre solution en utilisant une Map statique pour stocker les données. Cette solution est généralement compliquée car elle requiert une bonne prise en charge de certains points :

Il est aussi possible de créer une classe qui hérite de la classe Thread et qui encapsule les données qui lui sont associées.

Exemple :
public class MonThread extends Thread {
    
  public String valeur;

  @Override
  public void run() {
    // traitement du thread
  }
}

Pour obtenir une donnée, il suffit d'invoquer la méthode Thread.currentThread() et de caster le résultat vers le type du thread pour avoir un accès à la variable.

Exemple :
    MonThread thread = (MonThread) Thread.currentThread();
    System.out.println(thread.valeur);

Cette solution est une des plus performante mais elle impose de pouvoir utiliser sa propre instance de thread ce qui n'est pas le cas par exemple lors de l'utilisation de frameworks.

Enfin, l'API Java Core propose la classe ThreadLocal. Une même instance de type ThreadLocal permet de stocker et obtenir différentes valeurs, une pour chaque thread. La valeur est dédiée au thread courant : n'importe quel code exécuté dans le thread peut avoir accès à la valeur et la modifier au besoin mais il n'est pas possible d'obtenir les valeurs des autres threads dans le thread courant. Cette solution est toujours utilisable mais sa simplicité de mise en oeuvre peut parfois masquer certains points qu'il est important de prendre en compte pour éviter des effets de bords.

Ce chapitre contient plusieurs sections :

 

38.1. La classe ThreadLocal

Il peut être pratique de vouloir stocker une donnée qui soit contextuelle à un thread : c'est le rôle de la classe ThreadLocal. La classe ThreadLocal permet d'encapsuler des données qui seront accessibles par tous les traitements exécutés dans le thread. Elle a été ajoutée à Java depuis sa version 1.2.

La classe ThreadLocal implémente un mécanisme qui permet d'associer une donnée à un thread et de permettre son accès dans tous les traitements exécutés par le thread. Ceci permet de faciliter l'accès à des données contextuelles (contexte transactionnel, sécurité, Locale, ...) par ses traitements sans avoir à les passer en paramètres à chaque méthode invoquée.

Un ThreadLocal peut être considéré comme un scope supplémentaire, en plus des scopes existants (application, session, request) dont la portée est le thread. Une instance stockée dans un ThreadLocal ne peut être utilisée que par le thread qui l'y a déposé. Elle permet de définir des variables dont chaque thread possédera sa propre instance et un autre thread ne peut pas avoir accès à une instance qui n'est pas la sienne.

L'utilisation d'un ThreadLocal peut avoir plusieurs objectifs :

A partir de Java 5, la classe ThreadLocal est typée avec un generic pour assurer un control sur le type.

L'utilisation d'un ThreadLocal peut être extrêmement pratique mais aussi être à l'origine de soucis notamment de fuites de mémoires si certaines précautions ne sont pas prises.

De nombreux frameworks utilisent des ThreadLocal pour stocker des données contextuelles à chaque thread.

 

38.1.1. L'utilisation de la classe ThreadLocal

La classe ThreadLocal permet de stocker une variable qui ne pourra être accédée que par un seul thread. Même si plusieurs threads utilisent la même instance de ThreadLocal pour obtenir la variable, chaque thread obtiendra celle qui lui est associée.

Pour créer une instance de type ThreadLocal, il suffit d'utiliser l'opérateur new.

Exemple ( code Java 1.2 ) :
private ThreadLocal monThreadLocal = new ThreadLocal();

Une seule instance de type ThreadLocal est requise : la création de cette instance n'a besoin d'être réalisée qu'une seule fois pour tous les threads de la JVM quel que soit le thread qui la créée.

Exemple ( code Java 1.2 ) :
package fr.jmdoudoux.dej.thread;
      
public class TestThreadLocal {

  public static void main(String[] args) {
    MonTraitementAvecTL monTraitementAvecTL = new MonTraitementAvecTL();
    Thread thread1 = new Thread(monTraitementAvecTL);
    Thread thread2 = new Thread(monTraitementAvecTL);
    thread1.start();
    thread2.start();
  }
}

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
      
public class MonTraitementAvecTL implements Runnable {
  private ThreadLocal<String> monThreadLocal = new ThreadLocal<String>();
  
  public void run() {
    System.out.println("Mon traitement " + Thread.currentThread().getName()
        + " monThreadLocal=" + monThreadLocal);
    monThreadLocal.set("Valeur pour " + Thread.currentThread().getName());
    try {
      Thread.sleep(1000);
      String maValeur = monThreadLocal.get();
      System.out.println("Valeur = " + maValeur);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Résultat :
Mon traitement Thread-0 monThreadLocal=java.lang.ThreadLocal@1bc4459
Mon traitement Thread-1 monThreadLocal=java.lang.ThreadLocal@1bc4459
Valeur = Valeur pour Thread-0
Valeur = Valeur
pour Thread-1

Dans l'exemple ci-dessus, une seule instance de type MonTraitementAvecTL est créée, donc une seule instance de type ThreadLocal est créée.

Il est cependant généralement préférable de déclarée la variable ThreadLocal statique même si au premier abord cela peut paraître surprenant de définir une variable statique pour obtenir une instance contextuelle. C'est d'ailleurs une recommandation dans la Javadoc de la classe ThreadLocal.

Exemple :
package fr.jmdoudoux.dej.thread;
      
public class MonTraitementAvecTL implements Runnable {
  private static ThreadLocal<String> monThreadLocal = new ThreadLocal<String>();
  
  public void run() {
    System.out.println("Mon traitement " + Thread.currentThread().getName()
        + " monThreadLocal=" + monThreadLocal);
    monThreadLocal.set("Valeur pour " + Thread.currentThread().getName());
    try {
      Thread.sleep(1000);
      String maValeur = monThreadLocal.get();
      System.out.println("Valeur = " + maValeur);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Exemple :
package fr.jmdoudoux.dej.thread;
public class TestThreadLocal {

  public static void main(String[] args) {
    Thread thread1 = new Thread(new MonTraitementAvecTL());
    Thread thread2 = new Thread(new MonTraitementAvecTL());
    thread1.start();
    thread2.start();
  }
}

Résultat :
Mon traitement Thread-0 monThreadLocal=java.lang.ThreadLocal@1bc4459
Mon traitement
Thread-1 monThreadLocal=java.lang.ThreadLocal@1bc4459
Valeur = Valeur
pour Thread-1
Valeur = Valeur
pour Thread-0

Si chaque thread possède son instance cela fonctionne aussi mais le ThreadLocal perd de son intérêt puisque chaque instance ne va permettre que l'accès à la variable du thread.

Exemple :
package fr.jmdoudoux.dej.thread;
      
public class MonTraitementAvecTL implements Runnable {
  private ThreadLocal<String> monThreadLocal = new ThreadLocal<String>();

  public void run() {
    System.out.println("Mon traitement " + Thread.currentThread().getName()
        + " monThreadLocal=" + monThreadLocal);
    monThreadLocal.set("Valeur pour " + Thread.currentThread().getName());
    try {
      Thread.sleep(1000);
      String maValeur = monThreadLocal.get();
      System.out.println("Valeur = " + maValeur);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Exemple :
package fr.jmdoudoux.dej.thread;
      
public class TestThreadLocal { 

  public static void main(String[] args) {
    Thread thread1 = new Thread(new MonTraitementAvecTL());
    Thread thread2 = new Thread(new MonTraitementAvecTL());
    thread1.start();
    thread2.start();
  }
}

Résultat :
Mon traitement Thread-0 monThreadLocal=java.lang.ThreadLocal@150bd4d
Mon traitement Thread-1 monThreadLocal=java.lang.ThreadLocal@12b6651
Valeur = Valeur pour
Thread-1
Valeur = Valeur pour
Thread-0

Il ne faut surtout pas initialiser la valeur du ThreadLocal en utilisant un bloc d'initialisation static.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
 
public class MonCompteurTL {
 
  private static ThreadLocal<Integer> compteur = new ThreadLocal<Integer>();
 
  static {
    // surtout ne pas faire cela
    compteur.set(0);
  }
 
  public int get() {
    return compteur.get();
  }
 
  public int incrementer() {
    int valeur = compteur.get();
    valeur++;
    compteur.set(valeur);
    return valeur;
  }
  
  public void retirer() {
    compteur.remove();
  }
}

Celui-ci ne sera invoqué qu'une seule fois lors du chargement de la classe et la valeur 0 ne sera assignée au ThreadLocal que pour le thread courant. Lorsque les autres threads invoqueront la méthode get() pour la première fois sur le ThreadLocal ils obtiendront null et pas 0.

Pour que la valeur soit initialisée pour chaque thread, il faut redéfinir la méthode initialValue() pour qu'elle renvoie la valeur initiale. Celle-ci sera alors utilisée si le ThreadLocal ne possède pas encore de valeur pour le thread courant.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
 
public class MonCompteurTL {
 
  private static ThreadLocal<Integer> compteur = new ThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
      return Integer.valueOf(0);
    }
  };
 
  public int get() {
    return compteur.get();
  }
 
  public int incrementer() {
    int valeur = compteur.get();
    valeur++;
    compteur.set(valeur);
    return valeur;
  }
 
  public void retirer() {
    compteur.remove();
  }
}

Tous les threads peuvent accéder à l'instance de type ThreadLocal : seul le thread qui a ajouté une donnée en utilisant la méthode set() pourra obtenir cette variable qui lui est propre. Si plusieurs threads invoquent la méthode set() sur l'unique instance de ThreadLocal, chacun obtiendra la variable qu'il a passée en paramètre. Un thread ne peut pas obtenir la variable qui est associé à un autre thread.

Un ThreadLocal ne peut encapsuler qu'une variable pour un thread. Si plusieurs variables doivent être stockées pour un même thread, il faut déclarer une instance de type ThreadLocal pour chaque variable ou encapsuler les différentes variables dans une classe.

La méthode set() permet de préciser l'instance qui est associée au thread courant.

monThreadLocal.set("ma valeur");

La méthode get() permet d'obtenir l'instance qui est associée au thread courant.

String maValeur = (String) monThreadLocal.get();

Depuis Java 5, il est possible d'associer un type generic au type ThreadLocal, ce qui évite d'avoir à caster la valeur de retour de la méthode get().

Exemple ( code Java 5.0 ) :
private ThreadLocal monThreadLocal<String> = new ThreadLocal<String>();

monThreadLocal.set("ma valeur");
String valeur = monThreadLocal.get();

La méthode set() permet à chaque thread de stocker sa propre valeur : elle ne peut donc pas être utilisée pour mettre une valeur initiale. Pour définir une valeur initiale, il faut créer une classe fille de ThreadLocal et redéfinir la méthode initialValue().

Exemple :
private ThreadLocal monThreadLocal = new ThreadLocal<String>() {
  @Override protected String initialValue(){
    return "Valeur initiale";
  }
};

Si un thread n'a jamais utilisé la méthode set() de l'instance du ThreadLocal et invoque la méthode get(), il obtiendra en retour la valeur initiale.

 

38.1.2. Le fonctionnement interne d'un ThreadLocal

L'implémentation de la classe ThreadLocal ne permet qu'un accès aux variables du thread courant : il n'est donc pas possible d'accéder à toutes les variables de tous les threads.

La classe ThreadLocal est incluse dans le JDK à partir de la version 1.2. L'implémentation de la première version de la classe ThreadLocal souffrait d'un problème de performance lié à une forte contention lors de l'utilisation multithread. Elle a donc été revue dans la version 1.3 et 1.4 notamment pour permettre une amélioration de ses performances.

En Java 1.2, l'implémentation de ThreadLocal utilise une WeakHashMap dont les accès sont synchronized. Cependant cette implémentation souffre d'une forte contention et induit un surcoût en termes de performance notamment lorsque le nombre de threads s'accroit.

Le contrat de la classe ThreadLocal n'a pas changé en Java 1.3 mais son implémentation dans le JDK de Sun a été entièrement revue pour permettre une amélioration de ses performances en évitant la contention liée à la gestion des accès concurrents faite dans l'implémentation précédente avec synchronized. L'implémentation a été changée pour que la Map soit stockée dans le thread lui-même sous la forme d'un champ de type ThreadLocal.ThreadLocalMap nommé threadLocals qui n'est pas accessible directement. Du coup, il n'est plus utile de gérer les accès concurrents puisque seul le thread aura accès aux données.

Chaque thread possède deux propriétés de type ThreadLocal.ThreadLocalMap :

La clé de ces Map est l'instance de type ThreadLocal.

L'implémentation de la classe ThreadLocalMap stocke ses entrées dans un tableau de type ThreadLocalMap.Entry dont la taille initiale est de 16 éléments. Si la taille du tableau doit être agrandie, celle-ci est doublée.

Les données d'un ThreadLocal sont stockées dans la propriété threadLocals de type java.lang.ThreadLocal.ThreadLocalMap de chaque thread. La classe ThreadLocal.ThreadLocalMap encapsule les données de chaque ThreadLocal pour le thread courant. Son implémentation est similaire à une Map mais personnalisée et dédiée pour l'utilisation de ThreadLocal. C'est la raison pour laquelle c'est une classe interne. Les éléments stockés sont de type ThreadLocal.ThreadLocalMap.Entry qui hérite de WeakReference<ThreadLocal> . Elle encapsule :

Lorsque la méthode set() de la classe ThreadLocal est invoquée, elle recherche dans la map threadLocals du thread courant l'entrée dont la clé est l'instance elle-même et lui associé la valeur passée en paramètre.

Lorsque la méthode get() de la classe ThreadLocal est invoquée, elle recherche dans la map threadLocals du thread courant l'entrée dont la clé est l'instance elle-même et renvoie la valeur associée si elle est présente, sinon elle renvoie la valeur d'initialisation.

Lors de l'invocation de la méthode get() ou set() d'un ThreadLocal, un traitement est exécuté pour retrouver la valeur dans le ThreadLocalMap :

Pour faciliter la recherche de la clé dans la Map, chaque instance de type ThreadLocal possède une valeur pré-calculée qui favorise la recherche de l'instance de type ThreadLocal dans la map.

Chaque ThreadLocal possède un threadLocalHashCode dédié incrémenté de la valeur 0x61C88647 qui est aussi la valeur initiale. Ce threadLocalHashCode est utilisé pour déterminer l'index de l'entrée dans le tableau en appliquant la formule threadLocalHashCode & (taille_du_tableau -1).

La valeur 0x61c88647 est utilisée pour incrémenter la valeur de hachage de chaque entrée du thread local (une pour chaque thread qui en a besoin) afin de répartir au mieux les différentes valeurs de hachage des clés. C'est d'autant plus utile que la classe ThreadLocalMap n'utilise pas une collection pour son implémentation mais un tableau.

La classe ThreadLocal.ThreadLocalMap ne met en oeuvre aucun mécanisme dédié pour retirer les éléments inutilisés. Elle ne propose pas de mécanisme pour vérifier les clés de type WeakReference<ThreadLocal> qui pointent vers un objet inutilisé : ceci peut entraîner la conservation de références inutilisées pouvant provoquer une fuite de mémoire.

Cependant, lorsqu'un thread se termine, la JVM invoque sa méthode exit() qui remet à null ses propriétés threadLocals et inheritableThreadLocals : ceci garantit que les données liées au ThreadLocal seront récupérées par le ramasse-miettes.

 

38.1.3. ThreadLocal et fuite de mémoire

Il est nécessaire d'utiliser un ThreadLocal avec attention afin d'éviter d'avoir, sous certaines circonstances, des fuites de mémoire.

Le fait de ne pas déclarer static un ThreadLocal et donc de l'utiliser comme une variable d'instance peut aussi induire une certaine latence dans la récupération de la mémoire. L'implémentation de ThreadLocalMap utilise une clé ThreadLocalMap.Entry qui est une référence faible mais aucun thread démon n'existe pour les traiter. Ce sont les utilisations suivantes du ThreadLocal de chaque thread qui invoquent la méthode ThreadLocalMap.expungeStateEntries() qui retirent les clés inutilisées.

La classe ThreadLocal permet de fournir une instance d'un type dédiée à chaque thread. Chaque instance n'est donc accédée que par un seul thread et reste donc thread-safe. Tant que le thread est en cours d'exécution, toutes les données qui lui sont associées sont référencées par le ThreadLocal et donc le ramasse-miettes ne peut pas récupérer ces objets même s'ils ne sont plus utilisés par l'application car il reste au moins une référence.

Les différentes instances de chaque threads sont stockées dans une collection de type Map : ThreadLocalMap. Celle-ci utilise des références faibles (WeakReference) pour les clés. Le fait que les entrées soient des références faibles pourrait laisser croire qu'il est inutile de faire le ménage. Ces références faibles sont utilisées pour déterminer si l'instance du ThreadLocal peut être récupérée par le ramasse-miettes. Un prérequis est que plus aucune référence sur cette instance du thread n'existe dans la JVM : c'est généralement le cas lorsque le thread se termine.

La fin de l'exécution d'un Runnable ou d'un Callable ne signifie pas forcément la fin du thread : ce n'est pas le cas par exemple lorsque le thread fait partie d'un pool de threads. Un pool de threads est notamment utilisé par un java.util.concurrent.Executor ou par les serveurs d'applications.

Dans le cas de l'utilisation d'un pool de threads, la durée de vie d'un thread peut être très longue et durant cette période plusieurs tâches peuvent être exécutées. Si le threadLocal n'est pas nettoyé à la fin de chaque tâche, la tâche suivante obtient les valeurs stockées par la ou les tâches précédentes. C'est notamment le cas avec le pool de threads de traitement des servlets d'un conteneur web ou d'un ExecutorService.

 

38.1.3.1. Les fuites de mémoires dans un serveur d'applications

L'utilisation d'un ThreadLocal dans une application exécutée dans un serveur d'applications peut potentiellement conduire à une fuite de mémoire. Une mauvaise utilisation de ThreadLocal peut engendrer une fuite de ressources nommée classloader leaks qui se traduit généralement par un manque de mémoire dans la permgen.

C'est notamment le cas avec les serveurs d'applications qui utilisent un pool de threads pour réaliser différents traitements, par exemple le traitement des requêtes http avec des servlets. Si ces traitements utilisent un ou plusieurs ThreadLocal pour stocker des données contextuelles et que ces données ne sont pas explicitement retirées des threads locaux alors celles-ci sont conservées jusqu'à l'arrêt du serveur d'application.

Pour comprendre le problème, il faut se souvenir que sans être explicitement retirée du ThreadLocal, une donnée est conservée jusqu'à la fin du thread. Ceci même si l'application est arrêtée puisque le pool de thread n'est pas lié au cycle de vie de l'application mais au cycle de vie du conteneur dans lequel l'application s'exécute.

De plus, généralement, les classes des webapp sont chargées grâce à un classloader dédié. Chaque classe possède une référence sur le classloader qui l'a chargée. Si un ThreadLocal conserve une référence sur une instance d'une classe de l'application, possédant elle-même une référence sur l'instance du classloader, celle-ci ne peut pas être récupérée par le ramasse-miette. La classe du classloader reste chargée ainsi que toutes les classes qu'elle a chargées.

Un redéploiement de la webapp ne permet pas de supprimer les références ainsi que leur classloader : chaque déploiement va créer une nouvelle instance qui ne sera jamais récupérée par le ramasse-miettes. Ainsi le rechargement de l'application charge toutes les classes requises avec un nouveau classloader, mais toutes les classes du chargement précédent restent dans la permgen. Après plusieurs rechargements, dont le nombre dépend de la taille de la permgen et du nombre de classes chargées, il est possible que la taille de la permgen soit atteinte ce qui lève une exception de type OutOfMemoryError. Ce phénomène se nomme classloader leaks.

Il faut noter que les instances liées aux ThreadLocal et aux classloaders restent dans le heap mais cela ne pose généralement pas de soucis majeurs.

Cette problématique est d'autant plus fréquente dans les environnements de développement dans lesquels l'application est très fréquemment redéployée. Le conteneur web Apache Tomcat a ainsi tenté de pallier ces problèmes, notamment :

Cela ne signifie cependant pas qu'il ne faut pas utiliser de ThreadLocal mais qu'il est nécessaire de s'assurer que l'instance est retirée du ThreadLocal lorsque celle-ci n'est plus utile.

Le problème est que c'est généralement plus facile à dire qu'à faire :

Une solution consiste à utiliser un filtre dans une application web qui va retirer les instances du ou des ThreadLocal une fois qu'une requête HTTP est traitée. Ce filtre peut aussi être utilisé pour créer une instance et l'associer à un ThreadLocal.

Exemple ( code Java 5.0 ) :
public void doFilter(ServeletRequest request, ServletResponse) {
  try{
    // set ThreadLocal variable
    chain.doFilter(request, response)
  }finally{
    // remove threadLocal variable.
  }
}

 

38.1.3.2. Les fuites de mémoires dans un Executor

Il faut aussi être très prudent avec l'utilisation de ThreadLocal pour des tâches exécutées dans un Executor. Un Executor utilise un pool de threads pour exécuter les différentes tâches et ainsi limiter la création de threads. Il faut retirer les valeurs associées aux ThreadLocal car sinon la tâche suivante exécutée dans le thread va obtenir les valeurs stockées par la tâche précédente quand bien même celle-ci est terminée puisque ses valeurs sont stockées dans les maps du thread.

Pour déterminer à quel moment un objet associé à un ThreadLocal est récupéré par le ramasse-miettes, la méthode finalize() de sa classe est redéfinie.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
 
public class MonContexteTL extends ThreadLocal<MonContexte> {
 
  protected MonContexte initialValue() {
    return new MonContexte("Inconnu", 100);
  }
 
  @Override
  protected void finalize() throws Throwable {
    System.out.println("finalize() " + this);
    super.finalize();
  }
}

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
      
public class TestThreadLocal {
  private static final MonContexteTL monContextTL = new MonContexteTL();
  
  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
      @Override
      public void run() {
        System.out.println(monContextTL.get());
      }
    };
    thread.start();
    thread.join();
    executerGC();
    System.out.println("Fin");
  }
  
  private static void executerGC() throws InterruptedException {
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(1000);
    System.gc();
  }
}

Résultat :
fr.jmdoudoux.dej.thread.MonContexte@1bc4459 [user=Inconnu, valeur=100]
finalize() fr.jmdoudoux.dej.thread.MonContexte@1bc4459
[user=Inconnu, valeur=100]
Fin

Si le thread est dans un pool, le ThreadLocal n'est pas récupéré par le ramasse-miettes puisque le thread ne se termine pas à la fin de l'exécution des traitements.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadLocal {
  private static final MonContexteTL monContextTL = new MonContexteTL();
  public static void main(String[] args) throws InterruptedException {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(new Runnable() {
      public void run() {
        System.out.println(monContextTL.get());
      }
    });
    executerGC();
    executor.execute(new Runnable() {
      public void run() {
        System.out.println(monContextTL.get());
      }
    });
    executerGC();
    System.out.println("Fin");
  }
  
  private static void executerGC() throws InterruptedException {
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(1000);
    System.gc();
  }
}

Résultat :
fr.jmdoudoux.dej.thread.MonContexte@1820dda [user=Inconnu, valeur=100]
fr.jmdoudoux.dej.thread.MonContexte@1820dda [user=Inconnu, valeur=100]
Fin

La JVM ne s'arrête pas car le thread de l'Executor ne se termine pas : l'instance de MonContexte associée à ce thread n'est donc pas récupérée par le ramasse-miettes.

Il ne faut surtout pas oublier d'invoquer la méthode shutdown() de l'ExecutorService dès que celui-ci n'est plus nécessaire pour lui permettre de terminer l'exécution de ses threads.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadLocal {
  private static final MonContexteTL monContextTL = new MonContexteTL();
  public static void main(String[] args) throws InterruptedException {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.execute(new Runnable() {
      public void run() {
        System.out.println(monContextTL.get());
      }
    });
    executerGC();
    executor.execute(new Runnable() {
      public void run() {
        System.out.println(monContextTL.get());
      }
    });
    executor.shutdown();
    executerGC();
    System.out.println("Fin");
  }
  
  private static void executerGC() throws InterruptedException {
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(1000);
    System.gc();
    Thread.sleep(1000);
    System.gc();
  }
}

Résultat :
fr.jmdoudoux.dej.thread.MonContexte@1820dda [user=Inconnu, valeur=100]
fr.jmdoudoux.dej.thread.MonContexte@1820dda [user=Inconnu, valeur=100]
finalize() fr.jmdoudoux.dej.thread.MonContexte@1820dda
[user=Inconnu, valeur=100]
Fin

 

38.1.4. Bonnes pratiques pour l'utilisation d'un ThreadLocal

L'utilisation d'un ThreadLocal permet de facilement assurer un accès global à des objets dans tous les traitements du thread concerné. Il ne faut cependant pas abuser de cette facilité par exemple en ajoutant beaucoup d'objets. De plus, l'ajout de certains objets peut créer des dépendances qui ne sont pas forcément souhaitables.

Un ThreadLocal ne doit être utilisé que pour partager une donnée dans le contexte d'une exécution. Il ne doit pas être utilisé pour partager des données entre plusieurs exécutions.

Comme indiqué dans la Javadoc, il est recommandé de définir une instance de type ThreadLocal comme private static.

Il est toujours préférable que la méthode set() soit invoquée au moins une fois avant d'invoquer la méthode get(). L'initialisation d'un TheadLocal avec une valeur par défaut doit impérativement se faire en redéfinissant la méthode initialValue() : toute autre solution ne fonctionnera pas correctement.

Il y a bien sûr le cas où la valeur récupérée sera celle définie à l'initialisation mais il est aussi possible sous certaines circonstances d'obtenir une valeur définie par un autre traitement si celle-ci n'est pas retirée lorsqu'elle n'a plus d'utilité.

Attention : selon la manière dont le serveur d'applications charge les classes d'une webapp, il est même possible de partager des données entre plusieurs applications. Par exemple, si la webapp est déployée deux fois et que le serveur utilise le même classloader pour les deux webapp alors il est possible d'obtenir une valeur d'un ThreadLocal qui a été assignée par l'autre application.

Si cela est possible, il est préférable d'éviter d'utiliser les ThreadLocal dans les applications exécutées dans un serveur d'applications, sinon il faut prendre toutes les précautions nécessaires pour éviter les fuites de ressources.

Les objets encapsulés dans un ThreadLocal doivent être retirés lorsqu'ils ne sont plus utilisés pour permettre de supprimer la référence qui les lie au ThreadLocal et ainsi pouvoir éventuellement récupérer la mémoire grâce au ramasse-miettes. D'une manière générale, il est toujours important de retirer d'un ThreadLocal toutes les valeurs qui ne sont plus utiles dès que possible. Pour retirer la valeur d'un ThreadLocal associée au thread courant, il faut invoquer la méthode remove() qui va permettre de retirer l'entrée correspondante dans le ThreadLocalMap plutôt que la méthode set(null).

Le meilleur moyen de s'assurer que la donnée est retirée d'un ThreadLocal est d'invoquer la méthode remove() du ThreadLocal dans un bloc finally. Le bloc try correspondant doit englober les traitements qui invoquent la méthode set() du ThreadLocal. Ce bloc try/catch peut être utilisé par exemple :

Si plusieurs valeurs doivent être stockées et modifiées dans un ThreadLocal, il est préférable de les encapsuler dans une classe mutable et d'en stocker une instance dans le ThreadLocal.

Exemple ( code Java 5.0 ) :
private static ThreadLocal<Integer> monThreadLocal = new ThreadLocal<Integer>();
 
// ...
monThreadLocal.set(0);
// ...
int valeur = monThreadLocal.get();
valeur++;
monThreadLocal.set(valeur);

Il est plus performant de rechercher l'instance et de modifier la valeur encapsulée plutôt que de rechercher la valeur et la modifier (cette opération requiert une nouvelle recherche de l'entrée dans la map). Cela permet aussi de stocker des valeurs primitives directement sans avoir à utiliser un wrapper et l'autoboxin/unboxing.

Exemple ( code Java 5.0 ) :
class MaValeur {
   public int valeur;
}
 
// ...
private static ThreadLocal<MaValeur> monThreadLocal = new ThreadLocal<MaValeur>() {
            @Override
            protected MaValeur initialValue() {
                  return new MaValeur();
            }
      };
// ....
MaValeur maValeur = monThreadLocal.get();
maValeur.valeur++;

Remarque : l'exemple ci-dessus est volontairement simpliste pour ne garder que l'essentiel du principe de mise en oeuvre.

 

38.2. La classe InheritableThreadLocal

Ajoutée à Java 1.2, la classe InheritableThreadLocal permet de transmettre la valeur associée au thread courant à ses threads fils.

Elle hérite de la classe ThreadLocal. Elle ne possède que le constructeur par défaut.

Un thread peut lancer un autre thread : dans ce cas, le nouveau thread possède sa propre valeur dans le ThreadLocal qui sera vide par défaut.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
      
public class TestInheritableThreadLocal {
  public static void main(String[] args) throws InterruptedException {
    final ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    threadLocal.set("valeur1");
    afficherValeur(threadLocal);
    Thread t = new Thread() {
      public void run() {
        afficherValeur(threadLocal);
        threadLocal.set("valeur2");
        afficherValeur(threadLocal);
      }
    };
    t.start();
    t.join();
    afficherValeur(threadLocal);
  }
  
  private static void afficherValeur(final ThreadLocal<String> threadLocal) {
    System.out.println(Thread.currentThread().getName() + " : "
        + threadLocal.get());
  }
}

Résultat :
main :
valeur1
Thread-0 : null
Thread-0 : valeur2
main : valeur1

Il peut être utile d'avoir par défaut les valeurs du thread parent : c'est le rôle de la classe InheritableThreadLocal.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
      
public class TestInheritableThreadLocal {
  public static void main(String[] args) throws InterruptedException {
    final ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>();
    threadLocal.set("valeur1");
    afficherValeur(threadLocal);
    Thread t = new Thread() {
      public void run() {
        afficherValeur(threadLocal);
        threadLocal.set("valeur2");
        afficherValeur(threadLocal);
      }
    };
    t.start();
    t.join();
    afficherValeur(threadLocal);
  }
  
  private static void afficherValeur(final ThreadLocal<String> threadLocal) {
    System.out.println(Thread.currentThread().getName() + " : "
        + threadLocal.get());
  }
}

Résultat :
main : valeur1
Thread-0 : valeur1
Thread-0 :
valeur2
main : valeur1

Une fois le thread créé, il possède sa propre variable. Les valeurs de chaque threads sont stockées dans la propriété inheritableThreadLocals de type ThreadLocalMap.

La valeur de chaque thread reste donc indépendante : une modification de la valeur du thread parent après la création des threads fils ne modifie pas leur valeur correspondante. La valeur est simplement associée au thread au moment de sa création.

Elle ne définit qu'une seule méthode :

Méthode

Rôle

protected T childValue(T parentValue)

Fournir la valeur par défaut : elle renvoie la valeur associée au thread parent mais elle peut être redéfinie pour renvoyer une autre valeur


L'implémentation de la méthode childValue() renvoie simplement la valeur du thread parent passée en paramètre : donc, par défaut, la valeur est celle associée au thread parent si celle-ci est définie. Il est possible de redéfinir la méthode childValue() pour personnaliser la valeur initiale.

Exemple ( code Java 5.0 ) :
package fr.jmdoudoux.dej.thread;
      
public class TestInheritableThreadLocal {
  public static void main(String[] args) throws InterruptedException {
    final ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {
      @Override
      protected String childValue(String parentValue) {
        return parentValue + " fils";
      }
    };
    threadLocal.set("valeur");
    afficherValeur(threadLocal);
    Thread t = new Thread() {
      public void run() {
        afficherValeur(threadLocal);
      }
    };
    t.start();
    t.join();
  }
  
  private static void afficherValeur(final ThreadLocal<String> threadLocal) {
    System.out.println(Thread.currentThread().getName() + " : "
        + threadLocal.get());
  }
}

Résultat :
main : valeur
Thread-0 : valeur fils

Attention : lors de l'utilisation d'un InheritableThreadLocal, il ne faut stocker que des objets immuables ou thread-safe car ces objets sont partagés entre le thread parent et ses threads fils.

 

38.3. La classe ThreadLocalRandom

La classe java.util.concurrent.ThreadLocalRandom, ajoutée à Java 7, encapsule un générateur de nombres aléatoires qui est dédié uniquement au thread courant.

Son utilisation permet de limiter la contention liée à l'utilisation d'un seul générateur, avec l'invocation de la méthode random() de la classe Math, en associant à chaque thread sa propre instance du générateur, améliorant ainsi les performances lors de l'utilisation dans des traitements parallèles.

Elle hérite de la classe Random. Elle possède plusieurs méthodes :

Méthode

Rôle

static ThreadLocalRandom current()

Renvoyer l'instance de type ThreadLocalRandom associée au thread courant

protected int next(int bits)

Obtenir la prochaine valeur pseudo-aléatoire

double nextDouble(double n)

Obtenir la prochaine valeur flottante pseudo-aléatoire comprise entre 0 et la valeur fournie en paramètre

double nextDouble(double least, double bound)

Obtenir la prochaine valeur flottante pseudo-aléatoire comprise entre la première valeur fournie incluse et la seconde valeur fournie exclue

int nextInt(int least, int bound)

Obtenir la prochaine valeur entière pseudo-aléatoire comprise entre la première valeur fournie incluse et la seconde valeur fournie exclue

long nextLong(long n)

Obtenir la prochaine valeur entière pseudo-aléatoire comprise entre 0 et la valeur fournie en paramètre

long nextLong(long least, long bound)

Obtenir la prochaine valeur entière pseudo-aléatoire comprise entre la première valeur fournie incluse et la seconde valeur fournie exclue

void setSeed(long seed)

Lève une exception de type UnsupportedOperationException


Son utilisation est relativement simple :

Exemple ( code Java 7 ) :
long l = ThreadLocalRandom.current().nextLong(22L);

Il est aussi possible d'obtenir une valeur entre deux bornes fournies en paramètre d'une surcharge de la méthode nextXXX()

Exemple ( code Java 7 ) :
int i = ThreadLocalRandom.current().nextInt(10, 33);

Le générateur associé au Thread est initialisée avec une valeur (seed) calculée par le constructeur. Celle-ci ne peut plus être modifiée : la méthode setSeed() lève une exception de type UnsupportedOperationException si elle est invoquée après la création de l'instance par le constructeur.


37. Les threads 39. Le framework Executor Imprimer Index Index avec sommaire Télécharger le PDF    
Développons en Java
v 2.40   Copyright (C) 1999-2023 .