- Introduction aux défis de la concurrence
La programmation concurrentielle en Java confronte les développeurs à un problème central : la gestion des ressources partagées. Lorsque plusieurs fils d'exécution (threads) modifient une même variable sans synchronisation, des incohérences surviennent. Les solutions classiques comme synchronized ou Lock imposent un système de contrôle d'accès qui peut dégrader les performances et complexifier le code.
- Le concept de
ThreadLocal
ThreadLocal contourne ce problème non pas en synchronisant l'accès, mais en évitant le partage. Chaque thread possède sa propre copie de la variable. L'analogie classique est celle d'un casier personnel : chaque étudiant (thread) a son casier (ThreadLocal) où il range ses affaires, sans interférer avec les autres.
En Java, un ThreadLocal est généralement déclaré comme champ private static. Voici un exemple modifié :
public class ExempleThreadLocal {
private static ThreadLocal<String> contexte = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
contexte.set("utilisateur1");
System.out.println("Thread 1: " + contexte.get());
contexte.remove();
});
Thread t2 = new Thread(() -> {
contexte.set("utilisateur2");
System.out.println("Thread 2: " + contexte.get());
contexte.remove();
});
t1.start();
t2.start();
}
}
Chaque thread manipule sa propre valeur, sans conflit.
- Architecture interne : Thread, ThreadLocal, ThreadLocalMap
Le mécanisme repose sur trois éléments :
- Thread : chaque thread possède un champ
threadLocalsde typeThreadLocal.ThreadLocalMap. - ThreadLocal : la classe d'interface qui expose les méthodes
set(),get(),remove(). - ThreadLocalMap : une table de hachage interne qui stocke les paires (clé = référence faible vers
ThreadLocal, valeur = variable locale).
Lors d'un appel à set(valeur), le thread courant récupère sa propre map et y insère une entrée. get() fonctionne de manière symétrique. La clé est stockée via une référence faible (WeakReference) pour permettre le garbage collection du ThreadLocal lorsqu'il n'est plus utilisé.
- Le problème de la fuite mémoire
La fuite mémoire survient lorsque la valeur associée (stockée par référence forte dans l'entrée de la map) n'est pas libérée alors que le thread est toujours vivant, souvent dans un pool de threads. Même si la clé ThreadLocal est collectée, la valeur reste accessible tant que le thread existe, car l'entrée n'est pas nettoyée. Notez que ThreadLocalMap ne nettoie les entrées orphelines que lors des appels set()/get() sur ce même objet ThreadLocal. Si le ThreadLocal n'est plus référencé nulle part et que le thread continue à fonctionner, la valeur persiste en mémoire.
Exemple de scénario à risque :
public class FuitePossible {
private static final ThreadLocal<byte[]> cache = new ThreadLocal<>();
public static void traiter() {
cache.set(new byte[1024 * 1024]); // 1 Mo
// ... traitement ...
// Oublie de remove() : la référence persiste même après l'appel
}
}
Pour éviter cela, il faut systématiquement appeler remove() une fois le travail terminé, surtout dans un pool de threads où les threads sont réutilisés.
- Bonnes pratiques
- Toujours appeler
remove()dans un blocfinallyaprès utilisation. - Préférer
ThreadLocal.withInitial(() -> ...)pour fournir une valeur initiale propre. - Ne pas utiliser
ThreadLocalcomme substitut à un passage de paramètre explicite. - Dans les environnements web, associer la durée de vie du
ThreadLocalà celle de la requête.
- Cas d'usage typiques
- Connexions de base de données : chaque thread (ou requête) conserve sa propre connexion JDBC.
- Gestion de transactions : l'objet de transaction est partagé via
ThreadLocaldans toute la pile d'appels. - Contextes utilisateur (identité, locale) dans une application web.
- Formatteurs non thread-safe (comme
SimpleDateFormat) : une instance par thread.
- Comparaison avec d'autres mécanismes
| Mécanisme | Principe | Concurrence | Risques |
|---|---|---|---|
ThreadLocal |
Isolement par copie | Élevée | Fuite mémoire si oubli de remove |
synchronized |
Mutual exclusion | Faible | Deadlock |
volatile |
Visibilité mémoire | Moyenne | Pas d'atomicité |
Lock |
Verrou explicite | Moyenne | Deadlock si mal utilisé |
ThreadLocal est particulièrement adapté lorsque des données doivent rester confinées à un seul thread sans synchronisation lourde.
- Résumé
ThreadLocal est un outil puissant pour l'isolement de données par thread. Sa compréhension fine de l'architecture (references faibles, ThreadLocalMap) permet d'éviter des fuites mémoire. L'appel systématique à remove() dans un bloc finally est la règle d'or.