Starvation dans les applications mobiles — essence, causes et méthodes de prévention de la famine de thread

Auteur : IT Sectr Publié le : 2026-03-18 Temps de lecture : 10 min

Starvation (famine de thread) est une situation dans laquelle un thread ne peut pas accéder à une ressource nécessaire pour continuer son travail, bien qu'il soit prêt à s'exécuter. Selon Baeldung (Java Thread Starvation, 2024), la famine survient en raison d'une ordonnancement injuste, où les threads de faible priorité sont constamment reportés au profit de ceux de priorité plus élevée. Contrairement à Deadlock, Starvation ne bloque pas le thread — il reste dans l'état RUNNABLE mais n'obtient jamais de temps CPU.

Points Clés

  • Starvation — situation dans laquelle un thread ne peut pas accéder à une ressource malgré qu'il soit prêt à s'exécuter
  • Contrairement à Deadlock, un thread en famine reste dans l'état RUNNABLE — il n'est pas bloqué, mais ne progresse pas
  • Ordonnancement injuste (par exemple, synchronisation via synchronized) — la cause principale de Starvation sur la JVM
  • Fair Lock (ReentrantLock(true)) garantit un ordre FIFO équitable d'acquisition du verrou
  • Thread Priority dans le développement mobile est recommandé de ne pas modifier — Android Runtime gère les priorités elle-même

Qu'est-ce que Starvation ?

Starvation (famine de thread) est un problème de programmation multithread où un thread ne peut pas accéder à une ressource nécessaire pour accomplir sa tâche, bien que la ressource ne soit pas verrouillée en permanence par un autre thread. Le thread est dans l'état RUNNABLE, mais l'ordonnanceur ou le mécanisme de synchronisation reporte systématiquement son exécution au profit d'autres threads.

Dans le développement mobile, Starvation se manifeste comme une exécution inégale des tâches : certaines opérations s'exécutent instantanément, tandis que d'autres subissent des retards catastrophiques. Par exemple, un thread de synchronisation de données en arrière-plan peut ne jamais obtenir l'accès à la base de données si le thread UI et les gestionnaires d'animation le devancent constamment. Selon Android Developer Blog (Performance Matters, 2023), environ 12 % des images perdues (jank) sur Android sont causées par Starvation de tâches en arrière-plan dont dépend le rendu.

La différence clé entre Starvation et Deadlock est la réversibilité. Si la charge du système diminue ou si les priorités sont redistribuées, le thread affamé peut acquérir la ressource et terminer son travail. Cependant, sous une charge élevée soutenue, Starvation peut durer indéfiniment, créant l'impression d'une application gelée.

Causes de la famine de thread

Verrous injustes (Non-Fair Locks)

synchronized en Java et Kotlin est un exemple classique de mécanisme injuste. Sous forte contention, la JVM peut accorder continuellement le verrou aux mêmes threads actifs, tandis que d'autres threads perdent constamment la course. Ce n'est pas un bug de la JVM mais un compromis de conception : les verrous injustes offrent un débit plus élevé au détriment de l'équité d'accès. Pour les applications mobiles avec 4 à 8 threads, ce problème est particulièrement pertinent.

Utilisation incorrecte des priorités

Définir des priorités de threads différentes peut entraîner Starvation des threads de faible priorité. Dans Android Runtime, l'ordonnanceur CFS (Completely Fair Scheduler) de Linux distribue le temps CPU proportionnellement aux priorités, et si les threads de haute priorité sont constamment actifs, les threads de faible priorité peuvent ne jamais obtenir de temps CPU. Google déconseille fortement de modifier les priorités des threads dans Android — le système les gère lui-même.

Sections critiques longues

Si un thread maintient un verrou trop longtemps (effectuant des calculs lourds, des requêtes réseau ou des opérations fichier dans un bloc synchronized), les autres threads attendant ce verrou souffrent de famine. C'est particulièrement dangereux dans Android, où les opérations longues sur le thread UI provoquent des ANR, et les déplacer vers des threads d'arrière-plan sans optimiser les sections critiques ne fait que transférer le problème de Starvation aux threads workers.

Exemple de Starvation en code Kotlin

Considérons un exemple où un thread acquiert un verrou trop fréquemment en raison d'un ordonnancement injuste. Starvation est démontrée par une boucle infinie d'un thread de haute priorité qui empêche un thread de faible priorité d'accéder à une ressource partagée.

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id a obtenu l'accès")
            Thread.sleep(10)  // simulation de travail
        }
    }
}

fun main() {
    val resource = SharedResource()

    // Thread de haute priorité — constamment actif
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // Thread de faible priorité — peut ne jamais obtenir l'accès
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // "Low" peut ne jamais imprimer de message — Starvation !
}

Dans cet exemple, le thread highPriority acquiert constamment le verrou et ne le libère que pendant 10 ms. En raison de la nature injuste de synchronized, l'ordonnanceur JVM accordera très probablement le verrou à nouveau au même thread qui vient de le libérer — le thread de faible priorité souffre de famine. La solution est d'utiliser ReentrantLock(true) avec le flag fair, qui garantit un ordre d'attente FIFO.

La version corrigée avec un verrou équitable assure une distribution équitable de l'accès à la ressource.

kotlin
class FairSharedResource {
    private val fairLock = ReentrantLock(true)  // fair = true

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id a obtenu l'accès (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

Trois problèmes classiques de multithreading — Starvation, Deadlock et Livelock — sont souvent regroupés, mais leurs mécanismes et solutions diffèrent. Starvation — le thread est prêt mais ne peut pas acquérir la ressource. Deadlock — les threads sont bloqués par attente cyclique. Livelock — les threads sont actifs mais ne progressent pas.

ParamètreStarvationDeadlockLivelock
État du threadRUNNABLEBLOCKEDRUNNABLE
ProgrèsAucunAucunAucun (bien qu'actif)
Utilisation CPUFaibleMinimaleÉlevée (jusqu'à 100 %)
CauseOrdonnancement injusteAttente cycliqueMême réponse au conflit
Correction principaleFair Lock, raccourcir les sections critiquesHiérarchie de verrousLimite de tentatives, exponential backoff

Starvation est considéré moins critique que Deadlock car il n'est pas fatal — sous charge réduite, le thread affamé finira par s'exécuter. Cependant, dans l'utilisation réelle d'Android, où la mémoire et le CPU sont limités, Starvation peut durer des minutes, créant une expérience utilisateur inacceptable.

Comment détecter Starvation

Thread Dump pris à plusieurs reprises à intervalles courts — la méthode de base pour détecter la famine. Si un thread est constamment dans l'état RUNNABLE mais que sa pile d'appels ne change pas sur plusieurs dumps — c'est un signe classique de Starvation. Dans Android Studio, utilisez Android Profiler avec enregistrement de l'état des threads dans le temps.

La détection automatisée est possible via la surveillance du temps d'exécution des tâches. Si une tâche avec un temps d'exécution prévisible (par exemple, 50 ms) prend 5 secondes ou plus — il y a une forte probabilité de Starvation. Dans les applications mobiles, Firebase Performance Monitoring permet de configurer des traces personnalisées pour les sections critiques et de recevoir des notifications lorsque les seuils sont dépassés.

Pour diagnostiquer Starvation causée par des blocs synchronized, utilisez Java Flight Recorder (JFR) (disponible sur Android via l'API OpenJDK) ou Async Profiler. Ces outils montrent quels moniteurs ont les temps d'attente les plus élevés et quels threads sont en compétition pour chaque moniteur. Les données JFR s'intègrent avec IntelliJ IDEA Ultimate via son profileur intégré.

Méthodes de prévention de la famine de thread

Fair Lock (ReentrantLock avec flag true)

ReentrantLock(true) garantit que les threads acquièrent le verrou dans l'ordre FIFO. Contrairement à synchronized, un verrou équitable ne permet pas à un thread qui vient de libérer le verrou de le réacquérir immédiatement. Cela élimine complètement Starvation, bien que cela réduise le débit global de 10 à 20 % en raison de la surcharge de maintenance de la file d'attente.

Structures atomiques sans verrou

Les structures de données sans verrou (ConcurrentHashMap, AtomicReference, LongAdder) éliminent Starvation par définition, car elles n'ont pas de verrous qui peuvent être retenus par un seul thread. Toutes les opérations utilisent des instructions CAS du CPU qui garantissent qu'au moins un thread progresse en un nombre fini d'étapes. Pour le développement mobile, préférez ConcurrentLinkedQueue pour les files de tâches.

Sections critiques courtes

Minimiser le temps de rétention du verrou est un moyen universel de réduire le risque de Starvation. Déplacez les opérations lourdes (réseau, E/S disque, calculs complexes) hors des blocs synchronized. Utilisez ReadWriteLock pour les scénarios où les lecteurs ne doivent pas souffrir de famine en raison d'écrivains rares. La bibliothèque Kotlin Coroutines fournit Mutex avec un mécanisme de suspension qui ne bloque pas un thread OS.

Variables de condition et signaux

Condition.await() et signal() doivent être utilisés avec précaution : un thread attendant sur une Condition se réveille avec d'autres threads (spurious wakeup), et tous sont en compétition pour le verrou. Si un thread retourne immédiatement en attente après await tandis que d'autres parviennent à acquérir le verrou, le thread affamé peut se réveiller et se rendormir indéfiniment. Vérifiez toujours la condition dans une boucle while plutôt que dans un if pour garantir une revérification.

Questions Fréquentes

Quelle est la différence entre Starvation et l'inversion de priorité (Priority Inversion) ?

Priority Inversion est une situation où un thread de faible priorité maintient un verrou nécessaire à un thread de haute priorité. En conséquence, le thread de haute priorité attend le thread de faible priorité — les priorités sont inversées. Starvation est un problème plus large : un thread ne peut pas acquérir une ressource indépendamment de la priorité, en raison d'un ordonnancement injuste ou de longues sections critiques.

Starvation peut-elle se produire dans une application monothread ?

Non, Starvation est un problème de multithreading. Le code monothread n'a pas de contention de ressources ni d'ordonnancement de threads. Cependant, Starvation peut se produire dans du code asynchrone monothread (par exemple, la boucle d'événements JavaScript) si une micro-tâche retarde indéfiniment l'exécution d'autres via setTimeout avec un délai nul.

Comment le modèle mémoire Java est-il lié à Starvation ?

JMM (Java Memory Model) définit les règles de visibilité des changements entre les threads mais ne garantit pas un ordonnancement équitable. synchronized, conformément à JMM, assure la cohérence séquentielle — l'exactitude de base — mais ne prévient pas Starvation. L'équité nécessite des mécanismes supplémentaires non spécifiés dans JMM.

Qu'est-ce que Starvation dans le thread UI Android ?

Le thread UI (Main Thread) ne peut pas souffrir de famine au sens classique car il a la priorité la plus élevée. Cependant, Starvation se produit lorsque le thread UI attend un résultat d'un thread d'arrière-plan affamé. Un scénario typique : une AsyncTask ou coroutine charge des données mais ne peut pas accéder à la base de données en raison de la contention avec d'autres threads, et l'UI se fige en attendant.

Comment prévenir Starvation dans Kotlin Coroutines ?

Dans les coroutines, pour prévenir Starvation, utilisez limitedParallelism sur Dispatchers.IO pour éviter l'épuisement des threads. Pour la synchronisation, utilisez Mutex de kotlinx.coroutines.sync — il suspend la coroutine plutôt que de bloquer le thread, réduisant le risque de famine. Évitez runBlocking dans les coroutines, car il peut capturer un thread du pool et provoquer Starvation d'autres coroutines.

Résumé

  • Starvation — situation où un thread est prêt à s'exécuter mais ne peut pas acquérir une ressource en raison d'un ordonnancement injuste
  • Contrairement à Deadlock, un thread affamé reste dans l'état RUNNABLE et peut s'exécuter lorsque la charge diminue
  • Verrous injustes (synchronized) et utilisation incorrecte des priorités sont les principales causes de Starvation
  • Fair Lock (ReentrantLock avec flag true) garantit un ordre d'accès FIFO et élimine complètement la famine
  • Structures sans verrou (ConcurrentHashMap, AtomicReference) éliminent Starvation au niveau architectural
  • Thread Dump avec captures répétées et Java Flight Recorder sont des méthodes efficaces pour diagnostiquer Starvation
  • Sections critiques courtes et ReadWriteLock réduisent la probabilité de famine dans les systèmes à charge élevée

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.

Discuter du projet

Lisez aussi