Starvation in mobilen Anwendungen — Wesen, Ursachen und Methoden zur Vermeidung von Thread-Aushungerung

Autor: IT Sectr Veröffentlicht: 2026-03-18 Lesezeit: 10 Min.

Starvation (Thread-Aushungerung) ist eine Situation, in der ein Thread nicht auf eine für die Fortsetzung seiner Arbeit erforderliche Ressource zugreifen kann, obwohl er zur Ausführung bereit ist. Laut Baeldung (Java Thread Starvation, 2024) entsteht Aushungerung durch unfaire Planung, bei der Threads mit niedriger Priorität ständig zugunsten von Threads mit höherer Priorität zurückgestellt werden. Im Gegensatz zu Deadlock blockiert Starvation den Thread nicht — er bleibt im Zustand RUNNABLE, erhält aber nie CPU-Zeit.

Wichtige Erkenntnisse

  • Starvation — eine Situation, in der ein Thread trotz Bereitschaft zur Ausführung nicht auf eine Ressource zugreifen kann
  • Im Gegensatz zu Deadlock bleibt ein ausgehungerter Thread im Zustand RUNNABLE — er ist nicht blockiert, macht aber keine Fortschritte
  • Unfaire Planung (z.B. Synchronisation über synchronized) — die Hauptursache für Starvation auf der JVM
  • Fair Lock (ReentrantLock(true)) garantiert eine faire FIFO-Reihenfolge beim Sperrerwerb
  • Thread Priority sollte in der mobilen Entwicklung nicht geändert werden — die Android Runtime verwaltet Prioritäten selbst

Was ist Starvation?

Starvation (Thread-Aushungerung) ist ein Problem der Multithread-Programmierung, bei dem ein Thread nicht auf eine Ressource zugreifen kann, die zur Erledigung seiner Aufgabe erforderlich ist, obwohl die Ressource nicht dauerhaft von einem anderen Thread gesperrt ist. Der Thread befindet sich im Zustand RUNNABLE, aber der Scheduler oder Synchronisationsmechanismus verschiebt seine Ausführung systematisch zugunsten anderer Threads.

In der mobilen Entwicklung äußert sich Starvation als ungleiche Aufgabenausführung: Einige Operationen werden sofort ausgeführt, während andere katastrophale Verzögerungen erleiden. Beispielsweise kann ein Hintergrund-Thread zur Datensynchronisation nie auf die Datenbank zugreifen, wenn der UI-Thread und Animationshandler ihn ständig überholen. Laut Android Developer Blog (Performance Matters, 2023) werden etwa 12% der verpassten Frames (Jank) auf Android durch Starvation von Hintergrundaufgaben verursacht, von denen das Rendering abhängt.

Der Hauptunterschied zwischen Starvation und Deadlock ist die Umkehrbarkeit. Wenn die Systemlast sinkt oder Prioritäten neu verteilt werden, kann der ausgehungerte Thread die Ressource erwerben und seine Arbeit abschließen. Unter anhaltend hoher Last kann Starvation jedoch unbegrenzt andauern und den Eindruck einer eingefrorenen Anwendung erwecken.

Ursachen der Thread-Aushungerung

Unfaire Sperren (Non-Fair Locks)

synchronized in Java und Kotlin ist ein klassisches Beispiel für einen unfairen Mechanismus. Bei hohem Wettbewerb kann die JVM kontinuierlich dieselben aktiven Threads sperren, während andere Threads ständig im Wettlauf verlieren. Dies ist kein JVM-Fehler, sondern ein Design-Kompromiss: Unfaire Sperren bieten einen höheren Durchsatz auf Kosten der Zugangsfairness. Für mobile Anwendungen mit 4–8 Threads ist dieses Problem besonders relevant.

Falsche Verwendung von Prioritäten

Das Festlegen unterschiedlicher Thread-Prioritäten kann zu Starvation von Threads mit niedriger Priorität führen. In der Android Runtime verteilt der Linux CFS (Completely Fair Scheduler) die CPU-Zeit proportional zu den Prioritäten. Wenn Threads mit hoher Priorität ständig aktiv sind, erhalten Threads mit niedriger Priorität möglicherweise nie CPU-Zeit. Google rät dringend davon ab, Thread-Prioritäten in Android zu ändern — das System verwaltet sie selbst.

Lange kritische Abschnitte

Wenn ein Thread eine Sperre zu lange hält (schwere Berechnungen, Netzwerkanfragen oder Dateioperationen innerhalb eines synchronized-Blocks durchführt), hungern andere Threads, die auf diese Sperre warten, aus. Dies ist besonders gefährlich in Android, wo lange Operationen im UI-Thread ANR verursachen und deren Verschiebung in Hintergrund-Threads ohne Optimierung kritischer Abschnitte das Starvation-Problem lediglich auf Worker-Threads überträgt.

Starvation-Codebeispiel in Kotlin

Betrachten Sie ein Beispiel, bei dem ein Thread aufgrund unfairer Planung zu häufig eine Sperre erwirbt. Starvation wird durch eine Endlosschleife eines hochprioren Threads demonstriert, der einen niedrigprioren Thread am Zugriff auf eine gemeinsame Ressource hindert.

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id hat Zugriff erhalten")
            Thread.sleep(10)  // Arbeit simulieren
        }
    }
}

fun main() {
    val resource = SharedResource()

    // Hochpriorer Thread — ständig aktiv
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // Niedrigpriorer Thread — erhält möglicherweise nie Zugriff
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // "Low" gibt möglicherweise nie eine Nachricht aus — Starvation!
}

In diesem Beispiel erwirbt der highPriority-Thread ständig die Sperre und gibt sie nur für 10 ms frei. Aufgrund der unfairen Natur von synchronized wird der JVM-Scheduler höchstwahrscheinlich die Sperre erneut demselben Thread gewähren, der sie gerade freigegeben hat — der niedrigpriore Thread hungert aus. Die Lösung ist die Verwendung von ReentrantLock(true) mit dem fair-Flag, das eine FIFO-Wartereihenfolge garantiert.

Die korrigierte Version mit einer fairen Sperre gewährleistet eine gerechte Verteilung des Ressourcenzugriffs.

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

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id hat Zugriff erhalten (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

Drei klassische Multithreading-Probleme — Starvation, Deadlock und Livelock — werden oft gruppiert, aber ihre Mechanismen und Lösungen unterscheiden sich. Starvation — der Thread ist bereit, kann aber die Ressource nicht erwerben. Deadlock — Threads sind durch zyklisches Warten blockiert. Livelock — Threads sind aktiv, machen aber keine Fortschritte.

ParameterStarvationDeadlockLivelock
Thread-ZustandRUNNABLEBLOCKEDRUNNABLE
FortschrittKeinerKeinerKeiner (obwohl aktiv)
CPU-NutzungNiedrigMinimalHoch (bis zu 100%)
UrsacheUnfaire PlanungZyklisches WartenGleiche Reaktion auf Konflikt
HauptkorrekturFair Lock, kritische Abschnitte verkürzenSperrhierarchieWiederholungsgrenze, exponential backoff

Starvation gilt als weniger kritisch als Deadlock, da es nicht fatal ist — bei reduzierter Last wird der ausgehungerte Thread schließlich ausgeführt. Im realen Android-Einsatz, wo Speicher und CPU begrenzt sind, kann Starvation jedoch Minuten andauern und eine inakzeptable Benutzererfahrung schaffen.

Wie man Starvation erkennt

Thread Dump in kurzen Abständen wiederholt aufgenommen — die grundlegende Methode zur Erkennung von Aushungerung. Wenn ein Thread konsequent im Zustand RUNNABLE ist, sich sein Aufrufstapel jedoch über mehrere Dumps hinweg nicht ändert — ist dies ein klassisches Zeichen für Starvation. In Android Studio verwenden Sie den Android Profiler mit Aufzeichnung des Thread-Zustands über die Zeit.

Automatisierte Erkennung ist durch Überwachung der Ausführungszeit von Aufgaben möglich. Wenn eine Aufgabe mit vorhersagbarer Ausführungszeit (z.B. 50 ms) 5 Sekunden oder länger dauert — besteht eine hohe Wahrscheinlichkeit von Starvation. In mobilen Anwendungen ermöglicht Firebase Performance Monitoring die Einrichtung benutzerdefinierter Traces für kritische Abschnitte und Benachrichtigungen bei Überschreitung von Schwellenwerten.

Zur Diagnose von Starvation durch synchronized-Blöcke verwenden Sie Java Flight Recorder (JFR) (über die OpenJDK-API auf Android verfügbar) oder Async Profiler. Diese Tools zeigen, welche Monitore die höchsten Wartezeiten haben und welche Threads um jeden Monitor konkurrieren. JFR-Daten lassen sich über den integrierten Profiler in IntelliJ IDEA Ultimate integrieren.

Methoden zur Vermeidung von Thread-Aushungerung

Fair Lock (ReentrantLock mit true-Flag)

ReentrantLock(true) garantiert, dass Threads die Sperre in FIFO-Reihenfolge erwerben. Im Gegensatz zu synchronized erlaubt eine faire Sperre nicht, dass ein Thread, der die Sperre gerade freigegeben hat, sie sofort wieder erwirbt. Dies beseitigt Starvation vollständig, reduziert jedoch den Gesamtdurchsatz um 10–20% aufgrund des Overheads der Warteschlangenverwaltung.

Sperrfreie atomare Strukturen

Sperrfreie Datenstrukturen (ConcurrentHashMap, AtomicReference, LongAdder) beseitigen Starvation per Definition, da sie keine Sperren haben, die von einem Thread gehalten werden können. Alle Operationen verwenden CPU-CAS-Anweisungen, die den Fortschritt mindestens eines Threads in endlichen Schritten garantieren. Für die mobile Entwicklung bevorzugen Sie ConcurrentLinkedQueue für Aufgabenwarteschlangen.

Kurze kritische Abschnitte

Minimierung der Sperrhaltezeit ist ein universeller Weg, das Risiko von Starvation zu reduzieren. Verlagern Sie schwere Operationen (Netzwerk, Festplatten-I/O, komplexe Berechnungen) aus synchronized-Blöcken. Verwenden Sie ReadWriteLock für Szenarien, in denen Leser aufgrund seltener Schreiber nicht aushungern sollen. Die Kotlin Coroutines-Bibliothek bietet Mutex mit einem Suspendierungsmechanismus, der keinen OS-Thread blockiert.

Bedingungsvariablen und Signale

Condition.await() und signal() sollten mit Vorsicht verwendet werden: Ein auf einer Condition wartender Thread erwacht zusammen mit anderen Threads (spurious wakeup), und alle konkurrieren um die Sperre. Wenn ein Thread nach await sofort in den Wartezustand zurückkehrt, während andere die Sperre erlangen, kann der ausgehungerte Thread auf unbestimmte Zeit aufwachen und wieder einschlafen. Überprüfen Sie die Bedingung immer in einer while-Schleife statt in einer if-Anweisung, um eine erneute Überprüfung zu gewährleisten.

Häufig gestellte Fragen

Was ist der Unterschied zwischen Starvation und Priority Inversion?

Priority Inversion ist eine Situation, in der ein Thread mit niedriger Priorität eine Sperre hält, die ein Thread mit hoher Priorität benötigt. Infolgedessen wartet der hochpriore Thread auf den niedrigprioren — die Prioritäten werden umgekehrt. Starvation ist ein breiteres Problem: Ein Thread kann unabhängig von der Priorität aufgrund unfairer Planung oder langer kritischer Abschnitte keine Ressource erwerben.

Kann Starvation in einer Single-Thread-Anwendung auftreten?

Nein, Starvation ist ein Multithreading-Problem. Single-Thread-Code hat keine Ressourcenkonkurrenz oder Thread-Planung. Starvation kann jedoch in asynchronem Single-Thread-Code (z.B. JavaScript-Ereignisschleife) auftreten, wenn eine Mikroaufgabe die Ausführung anderer über setTimeout mit Nullverzögerung unbegrenzt verschiebt.

Wie hängt das Java-Speichermodell mit Starvation zusammen?

JMM (Java Memory Model) definiert Regeln für die Sichtbarkeit von Änderungen zwischen Threads, garantiert jedoch keine faire Planung. synchronized gewährleistet gemäß JMM sequenzielle Konsistenz — grundlegende Korrektheit — verhindert jedoch nicht Starvation. Fairness erfordert zusätzliche Mechanismen, die in JMM nicht spezifiziert sind.

Was ist Starvation im Android-UI-Thread?

Der UI-Thread (Main Thread) kann im klassischen Sinne nicht aushungern, da er die höchste Priorität hat. Starvation tritt jedoch auf, wenn der UI-Thread auf ein Ergebnis eines ausgehungerten Hintergrund-Threads wartet. Ein typisches Szenario: Ein AsyncTask oder eine Coroutine lädt Daten, kann aber aufgrund von Konkurrenz mit anderen Threads nicht auf die Datenbank zugreifen, und die UI friert beim Warten ein.

Wie verhindert man Starvation in Kotlin Coroutines?

Um Starvation in Coroutinen zu verhindern, verwenden Sie limitedParallelism auf Dispatchers.IO, um Thread-Erschöpfung zu vermeiden. Für die Synchronisation verwenden Sie Mutex aus kotlinx.coroutines.sync — es setzt die Coroutine aus, anstatt den Thread zu blockieren, was das Aushungerungsrisiko verringert. Vermeiden Sie runBlocking in Coroutinen, da es einen Pool-Thread erfassen und Starvation anderer Coroutinen verursachen kann.

Zusammenfassung

  • Starvation — eine Situation, in der ein Thread zur Ausführung bereit ist, aber aufgrund unfairer Planung keine Ressource erwerben kann
  • Im Gegensatz zu Deadlock bleibt ein ausgehungerter Thread im Zustand RUNNABLE und kann bei sinkender Last ausgeführt werden
  • Unfaire Sperren (synchronized) und falsche Prioritätsverwendung sind die Hauptursachen für Starvation
  • Fair Lock (ReentrantLock mit true-Flag) garantiert FIFO-Zugriffsreihenfolge und beseitigt Aushungerung vollständig
  • Sperrfreie Strukturen (ConcurrentHashMap, AtomicReference) beseitigen Starvation auf Architekturebene
  • Thread Dump mit wiederholten Aufnahmen und Java Flight Recorder sind effektive Methoden zur Starvation-Diagnose
  • Kurze kritische Abschnitte und ReadWriteLock reduzieren die Wahrscheinlichkeit von Aushungerung in hochbelasteten Systemen

Wir entwickeln eine mobile Applikation schlüsselfertig

IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.

Projekt besprechen

Lesen Sie auch