Livelock (verrouillage actif) est une situation en programmation multithread où les threads ne sont pas bloqués mais réagissent infiniment aux actions des uns et des autres sans effectuer de travail utile. Selon Baeldung (Java Concurrency Guide, 2024), dans Livelock les threads changent constamment d'état en réponse à l'état des threads voisins, mais aucun n'atteint son objectif. Contrairement à Deadlock, Livelock consomme 100% du CPU, ce qui décharge rapidement la batterie de l'appareil mobile.
Points clés
Livelock (verrouillage actif) est une situation dans un système multithread où les threads ne sont pas bloqués mais n'effectuent aucun travail utile. Chaque thread détecte qu'il ne peut pas continuer et tente de le corriger, mais ses actions provoquent la même réaction chez les autres threads. En conséquence, le système bascule infiniment entre les états sans progresser.
Une analogie classique de Livelock est celle de deux personnes se rencontrant dans un couloir étroit. Chacune essaie de se décaler pour laisser passer l'autre, mais toutes deux font le même mouvement simultanément et se retrouvent face à face. Elles ne restent pas immobiles (ce serait Deadlock), mais bougent activement, sans jamais réussir à se croiser. En programmation, cela correspond à des threads qui libèrent et réacquièrent constamment des ressources.
Dans le développement mobile, Livelock est particulièrement dangereux car il passe inaperçu pour l'utilisateur : l'application ne gèle pas, l'interface n'est pas bloquée, mais la batterie se décharge 2 à 3 fois plus vite en raison de la charge CPU à 100% par les threads d'arrière-plan. Selon les tests de Google (Android Battery Optimization, 2023), Livelock dans un Service d'arrière-plan peut réduire l'autonomie de la batterie de l'appareil de 40%.
Livelock survient lorsque plusieurs threads utilisent la même stratégie de réaction au conflit. Si le Thread A ne peut pas acquérir une ressource et libère sa ressource actuelle, tandis que le Thread B fait de même simultanément, les deux répètent le cycle — et la situation se répète infiniment. C'est particulièrement caractéristique des algorithmes avec TryLock et libération automatique en cas d'échec.
Lorsque les threads utilisent un délai fixe avant de réessayer, ils peuvent entrer dans un cycle synchrone. Si les deux threads attendent le même temps, ils tenteront simultanément d'acquérir la ressource et la libéreront simultanément à nouveau. Le problème est résolu en utilisant un exponential backoff avec une composante aléatoire (jitter), comme dans l'algorithme CSMA/CD en Ethernet.
Dans le développement mobile, Livelock survient souvent en raison d'une implémentation incorrecte des files de tâches. Par exemple, lorsqu'un thread worker termine le traitement d'un message mais, en raison de la logique de priorisation, transfère constamment le contrôle à un autre worker qui fait la même chose. Ces situations sont typiques des ThreadPoolExecutors personnalisés avec des politiques RejectedExecutionHandler non standard.
Considérons une situation où deux threads utilisent TryLock et libèrent la ressource en cas d'échec. Le verrouillage actif survient parce que les deux threads appliquent la même logique et réessayent de manière synchrone.
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.TimeUnit
class LivelockWorker(private val name: String,
private val lock1: ReentrantLock,
private val lock2: ReentrantLock) {
fun execute() {
while (true) {
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
println("$name — terminé !")
lock2.unlock()
lock1.unlock()
return
} else {
lock1.unlock() // libérer et réessayer
}
}
Thread.sleep(50) // même délai — facteur clé de Livelock
}
}
}
Si deux instances de LivelockWorker sont exécutées avec un ordre d'acquisition différent de lock1 et lock2, elles entreront en verrouillage actif. Chacune acquerra la première ressource, n'obtiendra pas la seconde, libérera la première, attendra 50 ms et réessayera — infiniment, en consommant du CPU. La correction consiste à ajouter une composante aléatoire au délai (jitter) et à limiter le nombre de tentatives.
La version corrigée utilise un exponential backoff avec jitter aléatoire. Après chaque tentative échouée, le temps d'attente augmente avec un multiplicateur aléatoire, brisant la synchronicité entre les threads.
fun executeWithBackoff() {
var delay = 10L
var attempts = 0
while (attempts < 5) {
if (lock1.tryLock(delay, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(delay, TimeUnit.MILLISECONDS)) {
println("Succès !")
lock2.unlock(); lock1.unlock()
return
}
lock1.unlock()
}
delay = (delay * 2 + (0..50).random())
attempts++
}
println("Échec après 5 tentatives")
}
Malgré leur similitude apparente, Livelock et Deadlock ont des mécanismes et des conséquences fondamentalement différents. Dans Deadlock, les threads sont bloqués et ne consomment pas de CPU — l'application se fige simplement. Dans Livelock, les threads sont actifs, consomment 100% du CPU, mais n'effectuent aucun travail utile. Le choix de la stratégie de résolution dépend de l'identification correcte du type de verrouillage.
| Paramètre | Deadlock | Livelock |
|---|---|---|
| État des threads | BLOCKED / WAITING | RUNNABLE |
| Consommation CPU | Minimale | Élevée (90-100%) |
| Consommation batterie | Faible | Élevée |
| Détection | Thread Dump | CPU Profiler + analyse visuelle |
| Cause typique | Ordre différent d'acquisition des verrous | Même stratégie de réaction au conflit |
| Correction | Hiérarchie des verrous | Retry limit + exponential backoff |
Dans le développement mobile, la différence pratique est énorme. Deadlock entraîne un ANR et un redémarrage de l'application — il est détecté et signalé via Google Play Console. Livelock passe inaperçu : l'application semble fonctionner, mais la batterie se vide en une heure, et l'utilisateur supprime simplement l'application. Selon Firebase Analytics (App Retention Report, 2024), 68% des utilisateurs suppriment une application si elle consomme excessivement la batterie en arrière-plan.
Détecter Livelock est plus difficile que Deadlock car le système ne donne aucun signal évident — pas d'exceptions, pas d'ANR, pas de messages d'erreur. La principale méthode de diagnostic est le CPU Profiler dans Android Studio. Si un thread est constamment à l'état RUNNABLE mais n'effectue aucune opération d'E/S ou de calcul utile — c'est une suspicion de Livelock.
Un indicateur supplémentaire est la consommation anormale de batterie lorsque l'application est inactive. Android Battery Historian (un outil du SDK Android) construit des graphiques de consommation d'énergie par composant. Si un CPU Wakelock est maintenu sans raison apparente — exécutez Method Tracing et analysez la pile d'appels des threads suspects.
Au niveau du code, la journalisation des tentatives avec threadId et timestamp aide. Si le journal montre des milliers de tentatives par seconde sans un seul succès — c'est Livelock. Il est recommandé d'implémenter un circuit breaker de type Hystrix ou un compteur de retry avec un seuil qui, lorsqu'il est dépassé, désactive l'opération et notifie le développeur via Crashlytics.
La méthode la plus simple et la plus fiable consiste à limiter le nombre de tentatives d'acquisition d'une ressource. Si l'opération échoue après N tentatives, le thread passe à l'état d'erreur et notifie l'utilisateur. N est choisi empiriquement : pour les applications mobiles, généralement 3 à 5 tentatives. Cela élimine complètement le Livelock infini au prix de rares faux positifs sous forte charge.
Au lieu d'un délai fixe entre les tentatives, on utilise une pause exponentiellement croissante avec une composante aléatoire. Formule : delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter). Cette approche brise non seulement la synchronicité des threads, mais réduit également la charge globale du système en cas de forte contention. Elle est utilisée dans les algorithmes de protocoles réseau et recommandée par Google pour la logique de retry de Firebase Realtime Database.
L'attribution de stratégies différentes à différents threads élimine la cause racine de Livelock — la réaction identique au conflit. Par exemple, un thread haute priorité acquiert la ressource sans la libérer, tandis qu'un thread basse priorité libère et attend. Dans le développement mobile, le thread UI peut avoir la priorité lors de l'acquisition des verrous, tandis que les threads workers d'arrière-plan utilisent TryLock avec timeout.
Dans certaines architectures, Livelock est évité au niveau de la conception : libération des ressources dans une seule direction. Par exemple, si le thread A transfère toujours le contrôle au thread B via un canal fixe, et que B ne tente jamais de rendre le contrôle à A — le cycle de réaction est impossible. L'architecture pipeline avec des étapes de traitement unidirectionnelles élimine complètement Livelock entre les étapes adjacentes dans Android CameraX et MediaPipe.
Questions fréquentes
Une boucle infinie ne dépend pas de facteurs externes et répète une seule opération sans interagir avec d'autres threads. Livelock est toujours une réaction aux actions d'autres threads : un thread modifie son comportement en réponse à l'état des threads voisins, créant une boucle de rétroaction fermée. Un Thread Dump en cas de Livelock montre des changements de contexte constants.
Dans les bases de données, Livelock survient lorsqu'une transaction est constamment reportée en raison de verrous d'autres transactions. Par exemple, le SGBD utilise l'algorithme wait-die : si une transaction avec un temps de démarrage plus ancien entre en conflit avec une plus récente, elle est annulée et redémarrée, mais rencontre le même conflit à chaque fois. Cela se résout avec un délai de redémarrage aléatoire.
Dans certains systèmes, Livelock est préférable à Deadlock car les threads restent actifs et peuvent détecter le problème. Par exemple, dans les algorithmes de verrouillage optimiste (optimistic locking), un comportement de type livelock est acceptable tant qu'une limite de tentatives garantit l'achèvement final. C'est un compromis entre performance et garantie de progression.
Livelock est extrêmement difficile à reproduire dans les tests car il nécessite un alignement précis des temporisations des threads. Les tests unitaires s'exécutent de manière déterministe et révèlent rarement un verrouillage actif. Il est recommandé d'utiliser des stress tests avec des exécutions répétées sous charge et une surveillance de la consommation CPU dans le profileur.
Sur un serveur, Livelock entraîne une dégradation des performances et des timeouts, mais le serveur évolue horizontalement. Sur Android, Livelock décharge la batterie et surchauffe l'appareil, créant la pire expérience utilisateur. De plus, les appareils mobiles ont un nombre limité de cœurs CPU, donc Livelock conduit plus rapidement à l'inopérabilité de l'ensemble du système.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi