1. Thread Dump et analyse de l’état des threads
Thread Dump (dump des threads) — c’est un instantané de l’état de tous les threads de l’application à un moment donné. C’est comme une photo de groupe de tous vos threads : qui fait quoi, qui est bloqué où, qui attend qui. Thread Dump — votre outil principal pour rechercher les deadlocks, livelocks et autres blocages mystérieux.
Comment obtenir un Thread Dump ?
Via le terminal (jstack) :
Si vous avez le PID du processus Java, exécutez :
jstack <PID>
La commande affiche dans la console l’état de tous les threads, en indiquant leur état et les moniteurs (verrous) qu’ils détiennent.
Via l’IDE (IntelliJ IDEA) :
Dans le menu « Run » → « Show Running List » → choisir le processus → « Thread Dump ».
Via VisualVM ou JConsole :
Ouvrez le processus, trouvez l’onglet « Threads » et prenez un instantané de l’état.
Exemple de Thread Dump
Extrait du dump :
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e0c7800 nid=0x1a48 waiting for monitor entry [0x000000001f00f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
- waiting to lock <0x00000000d6d6baf8> (a java.lang.Object)
- locked <0x00000000d6d6bb08> (a java.lang.Object)
On voit ici que le thread « Thread-1 » est bloqué (BLOCKED), qu’il détient un moniteur mais en attend un autre. Si vous voyez plusieurs threads de ce type, où l’un détient la ressource A et attend B, tandis qu’un autre détient B et attend A — c’est un deadlock classique.
États des threads
| Statut | Description |
|---|---|
| RUNNABLE | Le thread s’exécute ou est prêt à s’exécuter |
| BLOCKED | Attend la prise d’un moniteur (verrou) |
| WAITING | Attend notify()/notifyAll() (par exemple après un appel à wait()) |
| TIMED_WAITING | Attend avec délai d’expiration (par exemple sleep, wait(timeout)) |
| TERMINATED | Thread terminé |
Important : l’état RUNNABLE ne signifie pas toujours que le thread s’exécute à l’instant — il est simplement prêt à être exécuté (le planificateur de la JVM peut ne pas le lancer immédiatement).
Comment savoir que vous avez un deadlock ?
Dans le dump, plusieurs threads sont en état BLOCKED, chacun attendant un moniteur détenu par un autre thread du même ensemble.
À la fin du dump, jstack écrit généralement :
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000d6d6baf8 (object 0x00000000d6d6baf8, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00000000d6d6bb08 (object 0x00000000d6d6bb08, a java.lang.Object),
which is held by "Thread-1"
Si des threads restent longtemps en BLOCKED ou WAITING — c’est un motif d’enquête.
2. Surveillance et profilage des threads
VisualVM
VisualVM — un utilitaire gratuit inclus dans la plupart des JDK. Permet de se connecter à un processus, d’inspecter l’état des threads, de prendre un Thread Dump, de voir la charge CPU, les threads actifs et « bloqués ».
Onglet Threads : on voit combien de threads ont été créés, leurs états et l’historique d’activité.
Thread Dump : le bouton « Thread Dump » prend un instantané analogue à jstack.
Java Mission Control et Flight Recorder
Java Mission Control (JMC) : un outil avancé pour analyser la JVM en temps réel. Il aide à examiner les verrous, les temps d’exécution, les allocations et les latences.
Java Flight Recorder (JFR) : un profileur intégré à la JVM, qui collecte des événements sur les threads, les verrous, les pauses, etc.
Exemple : surveillance des verrous
Dans VisualVM ou JMC, on peut voir que :
- Le thread « A » est bloqué sur l’objet X.
- Le thread « B » détient l’objet X mais attend l’objet Y.
- Le thread « C » détient l’objet Y mais attend l’objet X.
C’est un interblocage circulaire classique (deadlock).
Comment utiliser ces outils en pratique ?
- Lancez l’application avec l’option -XX:+FlightRecorder (ou utilisez simplement JDK 11+).
- Ouvrez JMC, connectez-vous au processus, démarrez un enregistrement (start recording).
- Analysez les « hot spots », les verrous longs et la contention entre threads.
3. Journalisation et traçage
Dans les programmes multithread, le débogage « à vue » finit par faire mal. Journalisez l’entrée/sortie des sections critiques (blocs synchronized), les opérations sur les variables partagées, les attentes et réveils des threads — vous saurez ainsi qui a pris ou relâché une ressource et quand.
Comment journaliser ?
- Utilisez les outils standard : java.util.logging, SLF4J, Log4j.
- Consignez le nom du thread : Thread.currentThread().getName().
- Enregistrez l’heure et les identifiants des threads.
- Journalisez les événements de prise/libération de verrous.
Exemple de journalisation
synchronized(lock) {
System.out.println(Thread.currentThread().getName() + " a acquis lock");
// section critique
System.out.println(Thread.currentThread().getName() + " quitte lock");
}
Utilisation des noms de threads
Donnez des noms explicites aux threads !
Thread t = new Thread(runnable, "MyWorker-1");
Exemple de traçage avec un logger
import java.util.logging.Logger;
public class Example {
private static final Logger logger = Logger.getLogger(Example.class.getName());
public void doWork() {
logger.info(Thread.currentThread().getName() + " a commencé son travail");
synchronized (this) {
logger.info(Thread.currentThread().getName() + " est entré dans synchronized");
// ...
}
logger.info(Thread.currentThread().getName() + " a terminé son travail");
}
}
4. Meilleures pratiques de diagnostic
Minimisez la portée des verrous
Maintenez les verrous le moins longtemps possible.
Mauvais exemple :
synchronized(lock) {
// E/S longues
// calculs complexes
// accès à la base de données
// ... puis seulement le travail sur les données partagées
}
Bon exemple :
// en dehors de synchronized : E/S longues, calculs
synchronized(lock) {
// uniquement le travail sur les données partagées
}
Utilisez des noms de threads
Des noms de threads explicites font gagner du temps lors de l’analyse des dumps et des logs.
Écrivez des tests de multithreading
Utilisez JUnit + CountDownLatch pour modéliser des scénarios concurrents.
CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
// ...
latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // on attend la fin des deux threads
Utilisez try-finally avec ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// section critique
} finally {
lock.unlock();
}
Ainsi, vous n’oublierez pas de relâcher le verrou même en cas d’exception. Pour éviter les interblocages, utilisez tryLock() avec un délai d’expiration.
Documentez pourquoi la synchronisation est nécessaire
Des commentaires du type « Ici, il faut synchronized, parce que… » aideront, avec le temps, à comprendre l’intention.
5. Pratique : analyse d’un deadlock dans un programme de test
Exemple de code avec deadlock
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1: a acquis lockA");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockB) {
System.out.println("Thread-1: a acquis lockB");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2: a acquis lockB");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockA) {
System.out.println("Thread-2: a acquis lockA");
}
}
}, "Thread-2");
t1.start();
t2.start();
}
}
Comment détecter un deadlock
- Lancez le programme — il se figera.
- Obtenez un thread dump (jstack ou via VisualVM).
- Trouvez « Thread-1 » et « Thread-2 » — vous verrez que chacun détient un verrou et en attend un autre.
- À la fin du dump, il y aura la section « Found one Java-level deadlock ».
Comment corriger
- Prenez toujours les verrous dans le même ordre.
- Utilisez ReentrantLock avec tryLock() et un délai d’expiration : si vous ne pouvez pas acquérir tous les verrous, relâchez et réessayez.
6. Erreurs typiques lors du diagnostic des programmes multithread
Erreur n°1 : incapacité à lire un thread dump. Les débutants ont peur du dump : « Qu’est-ce que ces stack traces et statuts bizarres ? » En réalité, il suffit de connaître les états principaux et de chercher BLOCKED/WAITING pour simplifier l’analyse.
Erreur n°2 : ignorer les noms de threads. Sans noms explicites, comprendre un dump — c’est comme chercher une aiguille dans une botte de foin. N’hésitez pas à nommer vos threads !
Erreur n°3 : blocs synchronized trop larges. Si vous synchronisez de gros morceaux de code, les threads se bloqueront plus souvent les uns les autres — cela se voit aux nombreux BLOCKED dans le dump.
Erreur n°4 : confusion entre RUNNABLE et un thread effectivement en cours d’exécution. RUNNABLE n’est pas toujours « en train de tourner » sur le processeur. Le planificateur de la JVM décide qui exécuter.
Erreur n°5 : ne pas utiliser les outils de surveillance. Beaucoup ne connaissent pas VisualVM, JMC, Flight Recorder et se débrouillent avec println. Utilisez les outils — ils simplifient grandement la vie.
Erreur n°6 : absence de journalisation des opérations critiques. Sans logs, comprendre qui a pris/relâché un verrou et quand est presque impossible.
Erreur n°7 : essayer de détecter les data races « à l’œil ». Les courses ne se manifestent pas toujours et pas immédiatement — utilisez des tests avec CountDownLatch, provoquez la contention via Thread.yield() et analysez l’état des variables partagées.
GO TO FULL VERSION