Starvation w aplikacjach mobilnych — istota, przyczyny i sposoby zapobiegania zagłodzeniu wątku

Autor: IT Sectr Opublikowano: 2026-03-18 Czas czytania: 10 min

Starvation (zagłodzenie wątku) — to sytuacja, w której wątek nie uzyskuje dostępu do zasobu niezbędnego do kontynuowania pracy, mimo że jest gotowy do wykonania. Według Baeldung (Java Thread Starvation, 2024), zagłodzenie powstaje z powodu niesprawiedliwego planowania, gdy wątki o niskim priorytecie są stale odkładane na rzecz wątków o wyższym priorytecie. W przeciwieństwie do Deadlock, Starvation nie blokuje wątku — pozostaje on w stanie RUNNABLE, ale nigdy nie otrzymuje czasu procesora.

Najważniejsze

  • Starvation — sytuacja, w której wątek nie uzyskuje dostępu do zasobu, mimo gotowości do wykonania
  • W przeciwieństwie do Deadlock, wątek podczas zagłodzenia pozostaje w stanie RUNNABLE — nie jest zablokowany, ale nie robi postępów
  • Niesprawiedliwe planowanie (np. synchronizacja przez synchronized) — główna przyczyna Starvation na JVM
  • Fair Lock (ReentrantLock(true)) gwarantuje sprawiedliwą kolejność dostępu do blokady w kolejce
  • Thread Priority w programowaniu mobilnym zaleca się nie zmieniać — Android Runtime sama zarządza priorytetami

Czym jest Starvation?

Starvation (zagłodzenie wątku) — to problem programowania wielowątkowego, w którym wątek nie może uzyskać dostępu do zasobu niezbędnego do wykonania zadania, mimo że zasób nie jest zablokowany przez inny wątek na stałe. Wątek znajduje się w stanie RUNNABLE, ale planista lub mechanizm synchronizacji systematycznie odkłada jego wykonanie na rzecz innych wątków.

W programowaniu mobilnym Starvation objawia się jako nierównomierne wykonywanie zadań: niektóre operacje wykonują się natychmiast, inne — z katastrofalnymi opóźnieniami. Na przykład, wątek tła synchronizujący dane może nigdy nie uzyskać dostępu do bazy danych, jeśli wątek UI i handlerzy animacji stale go wyprzedzają. Według Android Developer Blog (Performance Matters, 2023), około 12% przypadków pominiętych klatek (jank) na Androidzie jest spowodowanych Starvation zadań tła, od których zależy renderowanie.

Kluczowa różnica między Starvation a Deadlock — odwracalność. Jeśli obciążenie systemu spadnie lub priorytety zostaną rozdzielone, głodujący wątek może uzyskać zasób i zakończyć pracę. Jednak w warunkach stale wysokiego obciążenia Starvation może trwać nieokreślenie długo, sprawiając wrażenie zawieszonej aplikacji.

Przyczyny zagłodzenia wątku

Niesprawiedliwe blokady (Non-Fair Locks)

synchronized w Javie i Kotlinie — klasyczny przykład niesprawiedliwego mechanizmu. Przy wysokiej konkurencji JVM może w nieskończoność oddawać blokadę tym samym aktywnym wątkom, podczas gdy inne wątki stale przegrywają wyścig. To nie jest błąd JVM, ale cecha implementacji: niesprawiedliwe blokady zapewniają wyższą przepustowość kosztem równomierności dostępu. Dla aplikacji mobilnych z 4-8 wątkami ten problem jest szczególnie istotny.

Nieprawidłowe użycie priorytetów

Ustawienie różnych priorytetów wątków może prowadzić do Starvation wątków o niskim priorytecie. W Android Runtime planista CFS (Completely Fair Scheduler) Linuxa rozdziela czas procesora proporcjonalnie do priorytetów, a jeśli wątki o wysokim priorytecie są stale aktywne, wątki o niskim priorytecie mogą nigdy nie otrzymać CPU. Google kategorycznie nie zaleca zmieniania priorytetów wątków w Androidzie — system sam nimi zarządza.

Długie sekcje krytyczne

Jeśli wątek utrzymuje blokadę zbyt długo (wykonuje ciężkie obliczenia, zapytania sieciowe lub operacje plikowe wewnątrz bloku synchronized), inne wątki oczekujące na tę blokadę głodują. Jest to szczególnie niebezpieczne w Androidzie, gdzie długie operacje w wątku UI powodują ANR, a przeniesienie ich do wątków tła bez optymalizacji sekcji krytycznych przenosi problem Starvation na wątki robocze.

Przykład Starvation w kodzie Kotlin

Rozważmy przykład, w którym jeden wątek przechwytuje blokadę zbyt często z powodu niesprawiedliwego planowania. Starvation jest demonstrowane przez nieskończoną pętlę wątku o wysokim priorytecie, który nie pozwala wątkowi o niskim priorytecie uzyskać dostępu do wspólnego zasobu.

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id uzyskał dostęp")
            Thread.sleep(10)  // symulacja pracy
        }
    }
}

fun main() {
    val resource = SharedResource()

    // Wątek wysokiego priorytetu — aktywny stale
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // Wątek niskiego priorytetu — może nigdy nie uzyskać dostępu
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // „Low" może ani razu nie wypisać komunikatu — Starvation!
}

W tym przykładzie wątek highPriority stale przechwytuje blokadę i zwalnia ją tylko na 10 ms. Z powodu niesprawiedliwego charakteru synchronized, planista JVM z dużym prawdopodobieństwem będzie oddawać blokadę ponownie temu samemu wątkowi, który właśnie ją zwolnił — wątek o niskim priorytecie głoduje. Rozwiązaniem jest użycie ReentrantLock(true) z flagą fair, która gwarantuje kolejność w kolejce oczekiwania.

Poprawiona wersja z fair lock zapewnia sprawiedliwy rozkład dostępu do zasobu.

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

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id uzyskał dostęp (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

Trzy klasyczne problemy wielowątkowości — Starvation, Deadlock i Livelock — są często łączone, ale ich mechanizmy i sposoby eliminacji są różne. Starvation — wątek jest gotowy, ale nie otrzymuje zasobu. Deadlock — wątki są zablokowane przez cykliczne oczekiwanie. Livelock — wątki są aktywne, ale nie robią postępów.

ParametrStarvationDeadlockLivelock
Stan wątkuRUNNABLEBLOCKEDRUNNABLE
PostępNieNieNie (mimo aktywności)
Zużycie CPUNiskieMinimalneWysokie (do 100%)
PrzyczynaNiesprawiedliwe planowanieCykliczne oczekiwanieTaka sama reakcja na konflikt
Główne rozwiązanieFair Lock, zmniejszenie sekcji krytycznychHierarchia blokadLimit ponowień, backoff wykładniczy

Starvation jest uważane za mniej krytyczne niż Deadlock, ponieważ nie jest śmiertelne — przy spadku obciążenia głodujący wątek w końcu się wykona. Jednak w warunkach rzeczywistego użytkowania aplikacji Android, gdzie pamięć i CPU są ograniczone, Starvation może trwać minutami, tworząc niedopuszczalne UX.

Jak wykryć Starvation

Thread Dump z wielokrotnym zrzucaniem w krótkich odstępach czasu — podstawowa metoda wykrywania zagłodzenia. Jeśli wątek consistently znajduje się w stanie RUNNABLE, ale jego stos wywołań nie zmienia się przez kilka zrzutów — to klasyczny objaw Starvation. W Android Studio służy do tego Android Profiler z zapisem stanu wątków w czasie.

Automatyczne wykrywanie jest możliwe poprzez monitorowanie czasu wykonania zadań. Jeśli zadanie z predictable execution time (np. 50 ms) wykonuje się 5 sekund lub dłużej — istnieje wysokie prawdopodobieństwo Starvation. W aplikacjach mobilnych Firebase Performance Monitoring pozwala skonfigurować custom traces dla sekcji krytycznych i otrzymywać powiadomienia przy przekroczeniu wartości progowych.

Do diagnozowania Starvation z powodu bloków synchronized używaj Java Flight Recorder (JFR) (dostępnego na Androidzie przez OpenJDK API) lub Async Profiler. Te narzędzia pokazują, które monitory mają najdłuższy czas oczekiwania i które wątki konkurują o każdy monitor. Dane z JFR integrują się z IntelliJ IDEA Ultimate przez wbudowany profiler.

Metody zapobiegania zagłodzeniu wątku

Fair Lock (ReentrantLock z flagą true)

ReentrantLock(true) gwarantuje, że wątki otrzymują blokadę w kolejności kolejki (FIFO). W przeciwieństwie do synchronized, fair lock nie dopuszcza sytuacji, w której wątek, który właśnie zwolnił blokadę, natychmiast przechwytuje ją ponownie. To całkowicie eliminuje Starvation, choć zmniejsza ogólną przepustowość o 10-20% z powodu narzutów na utrzymanie kolejki.

Struktury atomowe bez blokad

Struktury danych Lock-free (ConcurrentHashMap, AtomicReference, LongAdder) eliminują Starvation z definicji, ponieważ nie zawierają blokad, które mogą być utrzymywane przez jeden wątek. Wszystkie operacje używają instrukcji CAS procesora, które gwarantują postęp co najmniej jednego wątku w skończonej liczbie kroków. Dla programowania mobilnego preferuj ConcurrentLinkedQueue dla kolejek zadań.

Krótkie sekcje krytyczne

Minimalizacja czasu utrzymywania blokady — uniwersalny sposób na zmniejszenie ryzyka Starvation. Wynoś ciężkie operacje (sieć, wejście-wyjście dyskowe, złożone obliczenia) poza blok synchronized. Używaj ReadWriteLock w scenariuszach, gdzie czytelnicy nie powinni głodować z powodu rzadkich pisarzy. Biblioteka Kotlin Coroutines udostępnia Mutex z mechanizmem suspending, który nie blokuje wątku systemu operacyjnego.

Zmienne warunkowe i sygnały

Condition.await() i signal() powinny być używane ostrożnie: wątek oczekujący na Condition budzi się razem z innymi wątkami (spurious wakeup) i wszystkie konkurują o blokadę. Jeśli jeden wątek po await natychmiast wraca do oczekiwania, a inne zdążą przechwycić blokadę — głodujący wątek może budzić się i zasypiać w nieskończoność. Zawsze sprawdzaj warunek w pętli while, a nie w if, aby zagwarantować ponowne sprawdzenie.

Często zadawane pytania

Jaka jest różnica między Starvation a odwróceniem priorytetu (Priority Inversion)?

Priority Inversion — to sytuacja, w której wątek o niskim priorytecie utrzymuje blokadę niezbędną wątkowi o wysokim priorytecie. W rezultacie wątek o wysokim priorytecie czeka na wątek o niskim priorytecie — priorytety się odwracają. Starvation — to szerszy problem: wątek nie otrzymuje zasobu niezależnie od priorytetu, z powodu niesprawiedliwego planowania lub długich sekcji krytycznych.

Czy Starvation może wystąpić w aplikacji jednowątkowej?

Nie, Starvation — to problem wielowątkowości. W kodzie jednowątkowym nie ma konkurencji o zasoby ani planowania wątków. Jednak Starvation może wystąpić w asynchronicznym kodzie jednowątkowym (np. pętla zdarzeń JavaScript), jeśli jedna mikrozadanie w nieskończoność odkłada wykonanie innych przez setTimeout z zerowym opóźnieniem.

Jak Java Memory Model jest powiązana ze Starvation?

JMM (Java Memory Model) określa zasady widoczności zmian między wątkami, ale nie gwarantuje sprawiedliwego planowania. synchronized zgodnie z JMM zapewnia sequential consistency — podstawową poprawność — ale nie zapobiega Starvation. Do sprawiedliwości potrzebne są dodatkowe mechanizmy, niewchodzące w skład specyfikacji JMM.

Czym jest Starvation w wątku UI Androida?

Wątek UI (Main Thread) nie może głodować w klasycznym sensie, ponieważ ma najwyższy priorytet. Jednak Starvation powstaje, gdy wątek UI czeka na wynik głodującego wątku tła. Typowy scenariusz: AsyncTask lub korutyna ładują dane, ale nie mogą uzyskać dostępu do bazy danych z powodu konkurencji z innymi wątkami, a UI zawiesza się w oczekiwaniu.

Jak zapobiegać Starvation w Kotlin Coroutines?

W korutynach do zapobiegania Starvation używaj limitedParallelism na Dispatchers.IO, aby uniknąć wyczerpania wątków. Do synchronizacji stosuj Mutex z kotlinx.coroutines.sync — zawiesza on korutynę, a nie blokuje wątku, co zmniejsza ryzyko głodzenia. Unikaj runBlocking w korutynach, ponieważ może przechwycić wątek puli i spowodować Starvation innych korutyn.

Podsumowanie

  • Starvation — sytuacja, w której wątek jest gotowy do wykonania, ale nie otrzymuje zasobu z powodu niesprawiedliwego planowania
  • W przeciwieństwie do Deadlock, podczas głodzenia wątek jest w stanie RUNNABLE i może się wykonać przy spadku obciążenia
  • Niesprawiedliwe blokady (synchronized) i nieprawidłowe użycie priorytetów — główne przyczyny Starvation
  • Fair Lock (ReentrantLock z flagą true) gwarantuje kolejność dostępu FIFO i całkowicie eliminuje głodzenie
  • Struktury Lock-free (ConcurrentHashMap, AtomicReference) eliminują Starvation na poziomie architektury
  • Thread Dump z wielokrotnym zrzucaniem i Java Flight Recorder — skuteczne metody diagnostyki Starvation
  • Krótkie sekcje krytyczne i ReadWriteLock zmniejszają prawdopodobieństwo głodzenia w systemach o wysokim obciążeniu

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również