Starvation в мобильных приложениях — суть, причины возникновения и способы предотвращения голодания потока

Автор: IT Sectr Опубликовано: 2026-03-18 Время чтения: 10 мин

Starvation (голодание потока) — это ситуация, в которой поток не получает доступа к ресурсу, необходимому для продолжения работы, хотя сам готов к выполнению. Согласно Baeldung (Java Thread Starvation, 2024), голодание возникает из-за несправедливого планирования, когда низкоприоритетные потоки постоянно откладываются в пользу более приоритетных. В отличие от Deadlock, Starvation не блокирует поток — он остаётся в состоянии RUNNABLE, но никогда не получает процессорного времени.

Главное

  • Starvation — ситуация, при которой поток не получает доступа к ресурсу, несмотря на готовность к выполнению
  • В отличие от Deadlock, поток при голодании остаётся в состоянии RUNNABLE — он не заблокирован, но не прогрессирует
  • Несправедливое планирование (например, синхронизация через synchronized) — основная причина Starvation на JVM
  • Fair Lock (ReentrantLock(true)) гарантирует справедливый порядок доступа к блокировке в порядке очереди
  • Thread Priority в мобильной разработке рекомендуется не изменять — Android Runtime сама управляет приоритетами

Что такое Starvation?

Starvation (голодание потока) — это проблема многопоточного программирования, при которой поток не может получить доступ к ресурсу, необходимому для выполнения задачи, хотя ресурс не заблокирован другим потоком навсегда. Поток находится в состоянии RUNNABLE, но планировщик или механизм синхронизации систематически откладывает его выполнение в пользу других потоков.

В мобильной разработке Starvation проявляется как неравномерное выполнение задач: одни операции выполняются мгновенно, другие — с катастрофическими задержками. Например, фоновый поток синхронизации данных может никогда не получить доступ к БД, если UI-поток и обработчики анимаций постоянно его опережают. По данным Android Developer Blog (Performance Matters, 2023), около 12% случаев пропущенных кадров (jank) на Android вызваны Starvation фоновых задач, от которых зависит рендеринг.

Ключевое отличие Starvation от Deadlock — обратимость. Если нагрузка на систему снижается или приоритеты перераспределяются, голодающий поток может получить ресурс и завершить работу. Однако в условиях постоянной высокой нагрузки Starvation может длиться неопределённо долго, создавая впечатление зависшего приложения.

Причины голодания потока

Несправедливые блокировки (Non-Fair Locks)

synchronized в Java и Kotlin — классический пример несправедливого механизма. При высокой конкуренции JVM может бесконечно отдавать блокировку одним и тем же активным потокам, в то время как другие потоки постоянно проигрывают в гонке. Это не баг JVM, а особенность реализации: несправедливые блокировки обеспечивают более высокую пропускную способность в ущерб равномерности доступа. Для мобильных приложений с 4-8 потоками эта проблема особенно актуальна.

Неправильное использование приоритетов

Установка разных приоритетов потоков может привести к Starvation низкоприоритетных потоков. В Android Runtime планировщик CFS (Completely Fair Scheduler) Linux распределяет процессорное время пропорционально приоритетам, и если высокоприоритетные потоки постоянно активны, низкоприоритетные могут никогда не получить CPU. Google категорически не рекомендует изменять приоритеты потоков в Android — система сама управляет ими.

Долгие критические секции

Если поток удерживает блокировку слишком долго (выполняет тяжёлые вычисления, сетевые запросы или файловые операции внутри synchronized-блока), другие потоки, ожидающие эту блокировку, голодают. Это особенно опасно в Android, где долгие операции в UI-потоке вызывают ANR, а перенос их в фоновые потоки без оптимизации критических секций переносит проблему Starvation на worker-потоки.

Пример Starvation в коде на Kotlin

Рассмотрим пример, в котором один поток захватывает блокировку слишком часто из-за несправедливого планирования. Starvation демонстрируется через бесконечный цикл высокоприоритетного потока, который не даёт низкоприоритетному получить доступ к общему ресурсу.

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id получил доступ")
            Thread.sleep(10)  // симуляция работы
        }
    }
}

fun main() {
    val resource = SharedResource()

    // Высокоприоритетный поток — активен постоянно
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // Низкоприоритетный поток — может никогда не получить доступ
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // "Low" может ни разу не вывести сообщение — Starvation!
}

В этом примере highPriority поток постоянно захватывает блокировку и освобождает её лишь на 10 мс. Из-за несправедливого характера synchronized планировщик JVM с высокой вероятностью будет отдавать блокировку снова тому же потоку, который только что её освободил, — низкоприоритетный поток голодает. Решение — использовать ReentrantLock(true) с флагом fair, который гарантирует порядок в очереди ожидания.

Исправленная версия с fair lock обеспечивает справедливое распределение доступа к ресурсу.

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

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id получил доступ (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

Три классические проблемы многопоточности — Starvation, Deadlock и Livelock — часто объединяют, но их механизмы и способы устранения различны. Starvation — поток готов, но не получает ресурс. Deadlock — потоки заблокированы циклическим ожиданием. Livelock — потоки активны, но не прогрессируют.

ПараметрStarvationDeadlockLivelock
Состояние потокаRUNNABLEBLOCKEDRUNNABLE
ПрогрессНетНетНет (хотя активен)
Потребление CPUНизкоеМинимальноеВысокое (до 100%)
ПричинаНесправедливое планированиеЦиклическое ожиданиеОдинаковая реакция на конфликт
Главное исправлениеFair Lock, уменьшение критических секцийИерархия блокировокRetry limit, exponential backoff

Starvation считается менее критичной, чем Deadlock, потому что она не фатальна — при снижении нагрузки голодающий поток всё же выполнится. Однако в условиях реального использования Android-приложений, где память и CPU ограничены, Starvation может длиться минутами, создавая неприемлемый UX.

Как обнаружить Starvation

Thread Dump с повторными снятиями через короткие интервалы — базовый метод обнаружения голодания. Если поток consistently находится в состоянии RUNNABLE, но его стек вызовов не меняется на протяжении нескольких дампов — это классический признак Starvation. В Android Studio для этого используется Android Profiler с записью состояния потоков во времени.

Автоматизированное обнаружение возможно через мониторинг времени выполнения задач. Если задача с predictable execution time (например, 50 мс) выполняется 5 секунд и более — высока вероятность Starvation. В мобильных приложениях Firebase Performance Monitoring позволяет настроить custom traces для критических секций и получать уведомления при превышении пороговых значений.

Для диагностики Starvation из-за synchronized-блоков используйте Java Flight Recorder (JFR) (доступен на Android через OpenJDK API) или Async Profiler. Эти инструменты показывают, какие мониторы имеют наибольшее время ожидания и какие потоки конкурируют за каждый монитор. Данные из JFR интегрируются с IntelliJ IDEA Ultimate через встроенный профилировщик.

Методы предотвращения голодания потока

Fair Lock (ReentrantLock с флагом true)

ReentrantLock(true) гарантирует, что потоки получают блокировку в порядке очереди (FIFO). В отличие от synchronized, fair lock не допускает ситуации, когда недавно освободивший блокировку поток сразу захватывает её снова. Это полностью устраняет Starvation, хотя снижает общую пропускную способность на 10-20% из-за накладных расходов на поддержание очереди.

Атомарные структуры без блокировок

Lock-free структуры данных (ConcurrentHashMap, AtomicReference, LongAdder) исключают Starvation по определению, поскольку в них нет блокировок, которые могут удерживаться одним потоком. Все операции используют CAS-инструкции процессора, которые гарантируют прогресс хотя бы одного потока за конечное число шагов. Для мобильной разработки prefer ConcurrentLinkedQueue для очередей задач.

Короткие критические секции

Минимизация времени удержания блокировки — универсальный способ снизить риск Starvation. Выносите тяжёлые операции (сеть, дисковый ввод-вывод, сложные вычисления) за пределы synchronized-блока. Используйте ReadWriteLock для сценариев, где читатели не должны голодать из-за редких писателей. Библиотека Kotlin Coroutines предоставляет Mutex с suspending-механизмом, который не блокирует поток ОС.

Условные переменные и сигналы

Condition.await() и signal() должны использоваться с осторожностью: поток, ожидающий на Condition, пробуждается вместе с другими потоками (spurious wakeup), и все они конкурируют за блокировку. Если один поток после await немедленно возвращается в ожидание, а другие успевают захватить блокировку — голодающий поток может пробуждаться и засыпать бесконечно. Всегда проверяйте условие в цикле while, а не в if, чтобы гарантировать повторную проверку.

Часто задаваемые вопросы

В чём разница между Starvation и обращённым приоритетом (Priority Inversion)?

Priority Inversion — это ситуация, когда низкоприоритетный поток удерживает блокировку, необходимую высокоприоритетному. В результате высокоприоритетный поток ждёт низкоприоритетный — приоритеты обращаются. Starvation — более широкая проблема: поток не получает ресурс независимо от приоритета, из-за несправедливого планирования или долгих критических секций.

Может ли Starvation возникнуть в однопоточном приложении?

Нет, Starvation — проблема многопоточности. В однопоточном коде нет конкуренции за ресурсы и планирования потоков. Однако Starvation может возникнуть в асинхронном однопоточном коде (например, JavaScript event loop), если одна микрозадача бесконечно откладывает выполнение других через setTimeout с нулевой задержкой.

Как Java Memory Model связана со Starvation?

JMM (Java Memory Model) определяет правила видимости изменений между потоками, но не гарантирует справедливого планирования. synchronized в соответствии с JMM обеспечивает sequential consistency — базовую корректность, — но не предотвращает Starvation. Для справедливости нужны дополнительные механизмы, не входящие в спецификацию JMM.

Что такое Starvation в Android UI-потоке?

UI-поток (Main Thread) не может голодать в классическом смысле, поскольку он имеет наивысший приоритет. Однако Starvation возникает, когда UI-поток ждёт результата от голодающего фонового потока. Типичный сценарий: AsyncTask или корутина загружают данные, но не могут получить доступ к БД из-за конкуренции с другими потоками, и UI зависает в ожидании.

Как предотвратить Starvation в Kotlin Coroutines?

В корутинах для предотвращения Starvation используйте limitedParallelism на Dispatchers.IO, чтобы избежать исчерпания потоков. Для синхронизации применяйте Mutex из kotlinx.coroutines.sync — он приостанавливает корутину, а не блокирует поток, что снижает риск голодания. Избегайте runBlocking в корутинах, поскольку он может захватить поток пула и вызвать Starvation других корутин.

Итоги

  • Starvation — ситуация, при которой поток готов к выполнению, но не получает ресурс из-за несправедливого планирования
  • В отличие от Deadlock, при голодании поток в состоянии RUNNABLE и может выполниться при снижении нагрузки
  • Несправедливые блокировки (synchronized) и неправильное использование приоритетов — основные причины Starvation
  • Fair Lock (ReentrantLock с флагом true) гарантирует порядок доступа FIFO и полностью устраняет голодание
  • Lock-free структуры (ConcurrentHashMap, AtomicReference) исключают Starvation на уровне архитектуры
  • Thread Dump с повторными снятиями и Java Flight Recorder — эффективные методы диагностики Starvation
  • Короткие критические секции и ReadWriteLock снижают вероятность голодания в высоконагруженных системах

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также