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) планирач Линукса расподјељује процесорско вријеме пропорционално приоритетима, и ако су нити високог приоритета стално активне, нити нижег приоритета можда никада неће добити CPU. Google категорички не препоручује мијењање приоритета нити на Android-у — систем сам управља њима.

Дуге критичне секције

Ако нит држи закључавање сувише дуго (извршава тешка израчунавања, мрежне захтјеве или операције са датотекама унутар synchronized блока), друге нити које чекају ово закључавање изгладњују. Ово је посебно опасно на Android-у, гдје дуге операције у UI нити изазивају ANR, а преношење у позадинске нити без оптимизације критичних секција преноси проблем Starvation на радничке нити.

Примјер 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 ms. Због неправедне природе 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, смањење критичних секцијаХијерархија закључавањаLimit понављања, exponential backoff

Starvation се сматра мање критичним од Deadlock-а, јер није фатално — при смањењу оптерећења, изгладњела нит ће се ипак извршити. Међутим, у условима стварног коришћења Android апликација, гдје су меморија и CPU ограничени, Starvation може трајати минутима, стварајући неприхватљив UX.

Како открити 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 путем уграђеног профилера.

Методе спречавања изгладњивања нити

Fair Lock (ReentrantLock са ознаком true)

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, да бисте гарантовали поновну провјеру.

Често постављана питања

Која је разлика између Starvation-а и обрнутог приоритета (Priority Inversion)?

Priority Inversion — то је ситуација у којој нит нижег приоритета држи закључавање неопходно нити вишег приоритета. Као резултат, нит вишег приоритета чека нит нижег приоритета — приоритети се обрћу. Starvation је шири проблем: нит не добија ресурс независно од приоритета, због неправедног планирања или дугих критичних секција.

Може ли Starvation настати у једнoнитној апликацији?

Не, 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 у корутинама, јер може заузети нит из pool-а и изазвати 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. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође