Une introduction à ThreadLocal en Java

1. Vue d'ensemble

Dans cet article, nous examinerons la construction ThreadLocal du package java.lang . Cela nous donne la possibilité de stocker des données individuellement pour le thread actuel - et de les envelopper simplement dans un type spécial d'objet.

2. API ThreadLocal

La construction TheadLocal nous permet de stocker des données qui ne seront accessibles que par un thread spécifique .

Disons que nous voulons avoir une valeur Integer qui sera regroupée avec le thread spécifique:

ThreadLocal threadLocalValue = new ThreadLocal();

Ensuite, lorsque nous voulons utiliser cette valeur à partir d'un thread, nous devons seulement appeler une méthode get () ou set () . En termes simples, nous pouvons penser que ThreadLocal stocke des données à l'intérieur d'une carte - avec le thread comme clé.

De ce fait, lorsque nous appelons une méthode get () sur le threadLocalValue , nous obtiendrons une valeur Integer pour le thread demandeur:

threadLocalValue.set(1); Integer result = threadLocalValue.get();

Nous pouvons construire une instance de ThreadLocal en utilisant la méthode statique withInitial () et en lui passant un fournisseur:

ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);

Pour supprimer la valeur de ThreadLocal , nous pouvons appeler la méthode remove () :

threadLocal.remove();

Pour voir comment utiliser correctement ThreadLocal , nous allons d'abord examiner un exemple qui n'utilise pas de ThreadLocal , puis nous réécrirons notre exemple pour tirer parti de cette construction.

3. Stockage des données utilisateur dans une carte

Considérons un programme qui a besoin de stocker les données contextuelles spécifiques à l'utilisateur par ID utilisateur donné:

public class Context { private String userName; public Context(String userName) { this.userName = userName; } }

Nous voulons avoir un thread par identifiant d'utilisateur. Nous allons créer une classe SharedMapWithUserContext qui implémente l' interface Runnable . L'implémentation dans la méthode run () appelle une base de données via la classe UserRepository qui renvoie un objet Context pour un userId donné .

Ensuite, nous stockons ce contexte dans le ConcurentHashMap clé par userId :

public class SharedMapWithUserContext implements Runnable { public static Map userContextPerUserId = new ConcurrentHashMap(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContextPerUserId.put(userId, new Context(userName)); } // standard constructor }

Nous pouvons facilement tester notre code en créant et en démarrant deux threads pour deux userIds différents et en affirmant que nous avons deux entrées dans la carte userContextPerUserId :

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start(); assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

4. Stockage des données utilisateur dans ThreadLocal

Nous pouvons réécrire notre exemple pour stocker l' instance de contexte utilisateur à l' aide d'un ThreadLocal . Chaque thread aura sa propre instance ThreadLocal .

Lorsque vous utilisez ThreadLocal , nous devons être très prudents car chaque instance de ThreadLocal est associée à un thread particulier. Dans notre exemple, nous avons un thread dédié pour chaque userId particulier , et ce thread est créé par nous, nous en avons donc le contrôle total.

La méthode run () récupère le contexte utilisateur et le stocke dans la variable ThreadLocal en utilisant la méthode set () :

public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocal userContext = new ThreadLocal(); private Integer userId; private UserRepository userRepository = new UserRepository(); @Override public void run() { String userName = userRepository.getUserNameForUserId(userId); userContext.set(new Context(userName)); System.out.println("thread context for given userId: " + userId + " is: " + userContext.get()); } // standard constructor }

Nous pouvons le tester en démarrant deux threads qui exécuteront l'action pour un userId donné :

ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1); ThreadLocalWithUserContext secondUser = new ThreadLocalWithUserContext(2); new Thread(firstUser).start(); new Thread(secondUser).start();

Après avoir exécuté ce code, nous verrons sur la sortie standard que ThreadLocal a été défini par thread donné:

thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

Nous pouvons voir que chacun des utilisateurs a son propre contexte .

5. ThreadLocal s et pools de threads

ThreadLocal fournit une API facile à utiliser pour limiter certaines valeurs à chaque thread. C'est un moyen raisonnable d'assurer la sécurité des threads en Java. Cependant, nous devons être très prudents lorsque nous utilisons ThreadLocal s et des pools de threads ensemble.

Afin de mieux comprendre la mise en garde possible, considérons le scénario suivant:

  1. Tout d'abord, l'application emprunte un thread du pool.
  2. Ensuite, il stocke certaines valeurs confinées au thread dans ThreadLocal du thread actuel .
  3. Une fois l'exécution en cours terminée, l'application renvoie le thread emprunté au pool.
  4. Après un certain temps, l'application emprunte le même thread pour traiter une autre demande.
  5. Étant donné que l'application n'a pas effectué les nettoyages nécessaires la dernière fois, elle peut réutiliser les mêmes données ThreadLocal pour la nouvelle demande.

Cela peut entraîner des conséquences surprenantes dans des applications hautement simultanées.

Une façon de résoudre ce problème consiste à supprimer manuellement chaque ThreadLocal une fois que nous avons fini de l'utiliser. Étant donné que cette approche nécessite des révisions de code rigoureuses, elle peut être sujette à des erreurs.

5.1. Extension de ThreadPoolExecutor

En fait, il est possible d'étendre la classe ThreadPoolExecutor et de fournir une implémentation de hook personnalisée pour les méthodes beforeExecute () et afterExecute () . Le pool de threads appellera la méthode beforeExecute () avant d'exécuter quoi que ce soit en utilisant le thread emprunté. D'un autre côté, il appellera la méthode afterExecute () après avoir exécuté notre logique.

Par conséquent, nous pouvons étendre la classe ThreadPoolExecutor et supprimer les données ThreadLocal dans la méthode afterExecute () :

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor { @Override protected void afterExecute(Runnable r, Throwable t) { // Call remove on each ThreadLocal } }

Si nous soumettons nos demandes à cette implémentation d' ExecutorService , nous pouvons être sûrs que l'utilisation de ThreadLocal et des pools de threads n'introduira pas de risques pour la sécurité de notre application.

6. Conclusion

Dans cet article rapide, nous examinions la construction ThreadLocal . Nous avons implémenté la logique qui utilise ConcurrentHashMap qui était partagée entre les threads pour stocker le contexte associé à un userId particulier . Ensuite, nous avons réécrit notre exemple pour tirer parti de ThreadLocal pour stocker des données associées à un userId particulier et à un thread particulier.

L'implémentation de tous ces exemples et extraits de code peut être trouvée sur GitHub.