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) планирач Линукса расподјељује процесорско вријеме пропорционално приоритетима, и ако су нити високог приоритета стално активне, нити нижег приоритета можда никада неће добити 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, смањење критичних секција | Хијерархија закључавања | Limit понављања, exponential backoff |
Starvation се сматра мање критичним од Deadlock-а, јер није фатално — при смањењу оптерећења, изгладњела нит ће се ипак извршити. Међутим, у условима стварног коришћења Android апликација, гдје су меморија и CPU ограничени, Starvation може трајати минутима, стварајући неприхватљив UX.
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-ом обезбјеђује sequential consistency — основну исправност — али не спречава Starvation. За правичност су потребни додатни механизми који нису дио спецификације JMM-а.
UI нит (Main Thread) не може изгладњивати у класичном смислу, јер има највиши приоритет. Међутим, Starvation настаје када UI нит чека резултат од изгладњеле позадинске нити. Типичан сценарио: AsyncTask или корутина учитавају податке, али не могу приступити бази података због конкуренције са другим нитима, и UI се замрзава у чекању.
У корутинама, за спречавање Starvation-а користите limitedParallelism на Dispatchers.IO да бисте избјегли исцрпљивање нити. За синхронизацију примијените Mutex из kotlinx.coroutines.sync — он суспендује корутину, а не блокира нит, што смањује ризик од изгладњивања. Избјегавајте runBlocking у корутинама, јер може заузети нит из pool-а и изазвати Starvation других корутина.
Закључци
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође