Starvation (гладуване на нишка) — това е ситуация, при която нишка не получава достъп до ресурса, необходим за продължаване на работата, въпреки че самата тя е готова за изпълнение. Според Baeldung (Java Thread Starvation, 2024), гладуването възниква поради несправедливо планиране, когато нишките с нисък приоритет постоянно се отлагат в полза на тези с по-висок приоритет. За разлика от Deadlock, Starvation не блокира нишката — тя остава в състояние RUNNABLE, но никога не получава процесорно време.
Основни точки
Starvation (гладуване на нишка) — това е проблем на многонишковото програмиране, при който нишка не може да получи достъп до ресурса, необходим за изпълнение на задача, въпреки че ресурсът не е блокиран завинаги от друга нишка. Нишката е в състояние RUNNABLE, но планировчикът или механизмът за синхронизация систематично отлага нейното изпълнение в полза на други нишки.
В мобилната разработка Starvation се проявява като неравномерно изпълнение на задачи: някои операции се изпълняват незабавно, други — с катастрофални закъснения. Например фонова нишка, синхронизираща данни, може никога да не получи достъп до базата данни, ако UI нишката и обработчиците на анимации постоянно я изпреварват. Според Android Developer Blog (Performance Matters, 2023), около 12% от случаите на пропуснати кадри (jank) на Android са причинени от Starvation на фонови задачи, от които зависи рендерирането.
Ключовата разлика между Starvation и Deadlock — обратимост. Ако натоварването на системата намалее или приоритетите се преразпределят, гладуващата нишка може да получи ресурса и да завърши работата. В условия на постоянно високо натоварване обаче, Starvation може да продължи неопределено дълго, създавайки впечатление за замръзнало приложение.
synchronized в Java и Kotlin — класически пример за несправедлив механизъм. При висока конкуренция JVM може безкрайно да дава заключването на едни и същи активни нишки, докато други нишки постоянно губят в надпреварата. Това не е грешка на JVM, а характеристика на имплементацията: несправедливите заключвания осигуряват по-висока пропускателна способност за сметка на равномерността на достъпа. За мобилни приложения с 4-8 нишки този проблем е особено актуален.
Задаването на различни приоритети на нишки може да доведе до Starvation на нишки с нисък приоритет. В Android Runtime планировчикът CFS (Completely Fair Scheduler) на Linux разпределя процесорното време пропорционално на приоритетите и ако нишките с висок приоритет са постоянно активни, нишките с нисък приоритет може никога да не получат CPU. Google категорично не препоръчва промяна на приоритетите на нишки в Android — системата сама ги управлява.
Ако нишка държи заключването твърде дълго (изпълнява тежки изчисления, мрежови заявки или файлови операции вътре в synchronized блок), други нишки, чакащи това заключване, гладуват. Това е особено опасно в Android, където дългите операции в UI нишката причиняват ANR, а преместването им във фонови нишки без оптимизиране на критичните секции прехвърля проблема Starvation към работните нишки.
Нека разгледаме пример, в който една нишка завзема заключването твърде често поради несправедливо планиране. Starvation се демонстрира чрез безкраен цикъл на нишка с висок приоритет, която не позволява на нишка с нисък приоритет да получи достъп до общия ресурс.
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 ms. Поради несправедливия характер на synchronized, планировчикът на JVM с голяма вероятност ще даде заключването отново на същата нишка, която току-що го е освободила — нишката с нисък приоритет гладува. Решението е да се използва ReentrantLock(true) с флаг fair, който гарантира реда в опашката за изчакване.
Коригираната версия с fair lock осигурява справедливо разпределение на достъпа до ресурса.
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, Deadlock и Livelock — често се обединяват, но механизмите и начините за отстраняването им са различни. Starvation — нишката е готова, но не получава ресурс. Deadlock — нишките са блокирани от циклично изчакване. Livelock — нишките са активни, но не напредват.
| Параметър | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Състояние на нишка | RUNNABLE | BLOCKED | RUNNABLE |
| Напредък | Не | Не | Не (въпреки че е активна) |
| Консумация на CPU | Ниска | Минимална | Висока (до 100%) |
| Причина | Несправедливо планиране | Циклично изчакване | Еднаква реакция на конфликт |
| Основно решение | Fair Lock, намаляване на критичните секции | Йерархия на заключванията | Лимит за повторение, експоненциален backoff |
Starvation се счита за по-малко критично от Deadlock, защото не е фатално — при намаляване на натоварването гладуващата нишка в крайна сметка ще се изпълни. В условията на реално използване на Android приложения обаче, където паметта и CPU са ограничени, Starvation може да продължи минути, създавайки неприемливо потребителско изживяване.
Thread Dump с повторни снемания на кратки интервали — основният метод за откриване на гладуване. Ако нишка последователно е в състояние RUNNABLE, но нейният стек за извиквания не се променя в продължение на няколко снемания — това е класически признак на Starvation. В Android Studio за това се използва Android Profiler със запис на състоянието на нишките във времето.
Автоматизираното откриване е възможно чрез мониторинг на времето за изпълнение на задачи. Ако задача с предвидимо време за изпълнение (например 50 ms) се изпълнява 5 секунди или повече — има голяма вероятност за Starvation. В мобилни приложения Firebase Performance Monitoring позволява конфигуриране на персонализирани проследявания (custom traces) за критични секции и получаване на известия при превишаване на прагови стойности.
За диагностика на Starvation, причинено от synchronized блокове, използвайте Java Flight Recorder (JFR) (достъпен на Android чрез OpenJDK API) или Async Profiler. Тези инструменти показват кои монитори имат най-дълго време за изчакване и кои нишки се конкурират за всеки монитор. Данните от JFR се интегрират с IntelliJ IDEA Ultimate чрез вградения профилировчик.
ReentrantLock(true) гарантира, че нишките получават заключването по реда на опашката (FIFO). За разлика от synchronized, fair lock не допуска ситуация, при която нишката, която току-що е освободила заключването, веднага го завзема отново. Това напълно елиминира Starvation, въпреки че намалява общата пропускателна способност с 10-20% поради допълнителните разходи за поддържане на опашката.
Lock-free структури от данни (ConcurrentHashMap, AtomicReference, LongAdder) по дефиниция изключват Starvation, тъй като не съдържат заключвания, които могат да се държат от една нишка. Всички операции използват CAS инструкции на процесора, които гарантират напредък на поне една нишка за краен брой стъпки. За мобилна разработка предпочитайте ConcurrentLinkedQueue за опашки от задачи.
Минимизиране на времето за задържане на заключването — универсален начин за намаляване на риска от Starvation. Изнесете тежките операции (мрежа, дисков вход-изход, сложни изчисления) извън synchronized блок. Използвайте ReadWriteLock за сценарии, при които четящите не трябва да гладуват поради редки пишещи. Библиотеката Kotlin Coroutines предоставя Mutex с механизъм за спиране (suspending), който не блокира нишката на операционната система.
Condition.await() и signal() трябва да се използват внимателно: нишката, чакаща на Condition, се събужда заедно с други нишки (spurious wakeup) и всички се конкурират за заключването. Ако една нишка след await веднага се върне към чакане, докато други успеят да завземат заключването — гладуващата нишка може да се събужда и заспива безкрайно. Винаги проверявайте условието в цикъл while, а не в if, за да гарантирате повторна проверка.
Често задавани въпроси
Priority Inversion — това е ситуация, при която нишка с нисък приоритет държи заключване, необходимо на нишка с висок приоритет. В резултат нишката с висок приоритет чака нишката с нисък приоритет — приоритетите се обръщат. Starvation е по-широк проблем: нишката не получава ресурс независимо от приоритета, поради несправедливо планиране или дълги критични секции.
Не, Starvation — проблем на многонишковостта. В еднонишковия код няма конкуренция за ресурси и планиране на нишки. Starvation обаче може да възникне в асинхронен еднонишков код (например JavaScript event loop), ако една микрозадача безкрайно отлага изпълнението на други чрез setTimeout с нулево закъснение.
JMM (Java Memory Model) дефинира правилата за видимост на промените между нишките, но не гарантира справедливо планиране. synchronized в съответствие с JMM осигурява последователна консистентност — основна коректност — но не предотвратява Starvation. За справедливост са необходими допълнителни механизми, които не са част от спецификацията на JMM.
UI нишката (Main Thread) не може да гладува в класически смисъл, тъй като има най-висок приоритет. Starvation обаче възниква, когато UI нишката чака резултат от гладуваща фонова нишка. Типичен сценарий: AsyncTask или корутина зареждат данни, но не могат да получат достъп до базата данни поради конкуренция с други нишки и UI замръзва в очакване.
В корутините, за предотвратяване на Starvation използвайте limitedParallelism на Dispatchers.IO, за да избегнете изчерпване на нишки. За синхронизация приложете Mutex от kotlinx.coroutines.sync — той спира корутината, не блокира нишката, което намалява риска от гладуване. Избягвайте runBlocking в корутини, тъй като може да заеме нишка от пула и да причини Starvation на други корутини.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също