CodeGym /Cours /JAVA 25 SELF /Deadlock : causes, exemples, résolution

Deadlock : causes, exemples, résolution

JAVA 25 SELF
Niveau 53 , Leçon 0
Disponible

1. Approfondissons le deadlock

Un deadlock (« interblocage ») — c’est une situation où deux threads ou plus s’attendent mutuellement indéfiniment : chacun détient une ressource et tente d’en obtenir une autre déjà occupée par un thread voisin. Au final, personne ne peut continuer, et le programme « gèle ». C’est comme deux voitures sur un pont étroit, face à face : tant que l’une ne recule pas, personne ne passe.

En Java, un deadlock n’est pas une exception (Exception), mais un « gel » de l’application. Les threads ne se terminent pas et ne plantent pas avec une erreur, ils s’attendent simplement les uns les autres à l’infini. C’est pourquoi ces erreurs sont perfides : elles n’apparaissent que « dans certaines circonstances ».

Quatre conditions d’apparition d’un deadlock

  • 1. Exclusion mutuelle (Mutual Exclusion)
    Une ressource ne peut être détenue que par un seul thread à la fois (par exemple, un objet moniteur, un fichier).
  • 2. Rétention et attente (Hold and Wait)
    Un thread détient une ressource et tente d’en obtenir une deuxième, sans relâcher la première.
  • 3. Pas de préemption (No Preemption)
    On ne peut pas « retirer » une ressource à un thread : seul le thread lui-même peut la libérer.
  • 4. Attente circulaire (Circular Wait)
    Il existe une chaîne fermée où chaque thread attend une ressource détenue par le suivant dans la chaîne.

Un deadlock n’est possible que si TOUTES ces conditions sont réunies simultanément. Il suffit d’en violer au moins une — et l’interblocage devient impossible.

2. Exemple de code avec deadlock

Schéma

  • Il y a deux ressources : lock1 et lock2 (objets ordinaires).
  • Le thread A acquiert d’abord lock1, puis tente d’obtenir lock2.
  • Le thread B acquiert d’abord lock2, puis tente d’obtenir lock1.

Exemple (ne faites pas ça chez vous !)

public class DeadlockDemo {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        // Thread 1
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 : a acquis lock1");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Thread 1 : tente d'acquérir lock2");
                synchronized (lock2) {
                    System.out.println("Thread 1 : a acquis lock2");
                }
            }
        });

        // Thread 2
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 : a acquis lock2");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                System.out.println("Thread 2 : tente d'acquérir lock1");
                synchronized (lock1) {
                    System.out.println("Thread 2 : a acquis lock1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

Que se passe-t-il au lancement ?

  • Le thread 1 acquiert lock1, le thread 2 — lock2.
  • Les deux « s’endorment » pendant 100 millisecondes, le temps de verrouiller le premier verrou.
  • Ensuite, chacun essaie d’acquérir le second verrou, déjà détenu par l’autre thread.
  • Les deux threads s’attendent mutuellement à l’infini — un deadlock s’est produit.

Dans la console, vous verrez :

Thread 1 : a acquis lock1
Thread 2 : a acquis lock2
Thread 1 : tente d'acquérir lock2
Thread 2 : tente d'acquérir lock1

Pourquoi un deadlock survient-il ? Analyse détaillée

  • Exclusion mutuelle : chaque verrou ne peut être détenu que par un seul thread.
  • Rétention et attente : le thread 1 détient lock1 et attend lock2 ; le thread 2 détient lock2 et attend lock1.
  • Pas de préemption : personne ne peut « arracher » le verrou de l’extérieur — il faut sortir du bloc synchronized.
  • Attente circulaire : l’attente s’est refermée en boucle entre deux threads.

3. Comment éviter et prévenir un deadlock

Acquérir les ressources dans le même ordre

Règle principale : si plusieurs threads doivent acquérir plusieurs ressources — faites-le toujours dans le même ordre. Par exemple : d’abord lock1, puis lock2 — partout dans le code.

public void doSomething() {
    Object firstLock = lock1;
    Object secondLock = lock2; // ordre unique "lock1 -> lock2"
    synchronized (firstLock) {
        synchronized (secondLock) {
            // Traitement avec les deux ressources
        }
    }
}

Avec un ordre unique, l’attente circulaire est impossible — la condition du deadlock est violée.

Utiliser tryLock avec délai d’attente (ReentrantLock)

Quand il est impossible de savoir à l’avance quelles ressources seront nécessaires, utilisez ReentrantLock et la méthode tryLock pour ne pas attendre indéfiniment.

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TryLockDemo {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public void doWork() {
        try {
            if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            // Section critique
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("Impossible d'acquérir lock2, on annule");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("Impossible d'acquérir lock1, on annule");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Avantage : on peut annuler l’opération et réessayer plus tard. Inconvénient : le code est plus complexe, mais le deadlock est exclu.

Minimisez le temps de maintien des verrous

Gardez le verrou le moins longtemps possible. À l’intérieur de synchronized/lock, ne faites que le nécessaire ; sortez tout le reste de la section critique.

Évitez les verrous imbriqués

Moins il y a de synchronized/lock imbriqués, plus le risque d’interblocage est faible. Si possible — utilisez un seul verrou pour des ressources liées.

Utilisez des structures thread-safe prêtes à l’emploi

La bibliothèque standard a déjà résolu de nombreux problèmes : des collections comme ConcurrentHashMap et d’autres classes de java.util.concurrent minimisent le risque de deadlock et simplifient le code.

4. Diagnostic d’un deadlock

Thread Dump et jstack

Un Thread Dump — un instantané de l’état de tous les threads dans la JVM.

Depuis la console :

jstack <pid>

<pid> est l’identifiant du processus Java (vous pouvez l’obtenir via jps).

Dans l’IDE, il y a généralement un bouton « Thread Dump » dans le panneau de débogage.

Dans le dump, recherchez :

  • Les statuts BLOCKED ou WAITING.
  • Des messages du type waiting to lock ... et locked ....
  • La phrase de la JVM : "Found one Java-level deadlock:" — lorsque l’interblocage est détecté.

Exemple d’extrait de dump :

"Thread-1":
  waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
  which is held by "Thread-2"
"Thread-2":
  waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
  which is held by "Thread-1"

Voilà un deadlock : les threads se « tiennent » mutuellement.

VisualVM et autres outils

  • VisualVM — un outil gratuit pour analyser les threads et détecter les deadlocks.
  • Java Mission Control / Flight Recorder — supervision et profilage avancés.

Dans VisualVM, vous pouvez voir l’arbre des threads, leurs états et recevoir une notification d’interblocage.

Tableau : « Comment ne pas tomber dans un deadlock »

Cause du deadlock Comment éviter
Prise des ressources dans un ordre différent Toujours respecter un ordre unique de prise des ressources
synchronized imbriqués Minimiser l’imbrication, utiliser un seul verrou si possible
Maintien du verrou trop long Réduire le travail à l’intérieur de la section critique
Utilisation simultanée de plusieurs verrous Utiliser tryLock avec délai d’attente et savoir annuler
Collections non thread-safe Utiliser des collections concurrentes (ConcurrentHashMap et autres)

5. Erreurs typiques avec les deadlocks

Erreur n° 1 : Prise des ressources dans des ordres différents. La cause la plus fréquente — des threads prennent les verrous dans des ordres différents. Même si « c’est plus rapide », respectez un ordre unique — sinon l’attente circulaire est garantie.

Erreur n° 2 : synchronized imbriqués sans nécessité. Une imbrication superflue augmente le risque d’interblocage et complexifie le code. Simplifiez votre modèle de verrous.

Erreur n° 3 : Ignorer tryLock et les délais d’attente. Un verrou synchronized fera attendre le thread indéfiniment. En cas d’incertitude, utilisez tryLock avec délai d’attente et une logique d’annulation.

Erreur n° 4 : Exécuter du code trop long sous verrou. Requêtes réseau, I/O et calculs lourds sous verrou augmentent fortement le risque de problèmes. Faites-les hors de la section critique.

Erreur n° 5 : Ne pas analyser le Thread Dump. Le Thread Dump est votre meilleur allié pour traquer les interblocages. Utilisez jstack et analysez les états BLOCKED/WAITING, ainsi que les chaînes « holding/waiting to lock ».

1
Mission
JAVA 25 SELF, niveau 53, leçon 0
Bloqué
Passage du témoin entre robots : risque d'interblocage 🤖
Passage du témoin entre robots : risque d'interblocage 🤖
1
Mission
JAVA 25 SELF, niveau 53, leçon 0
Bloqué
Échapper à l'interblocage : ingénieurs malins avec délai d'attente 🛠️
Échapper à l'interblocage : ingénieurs malins avec délai d'attente 🛠️
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION