Deadlock у мобилном развоју: шта је, узроци настанка и начини избегавања узајамног блокирања

Аутор: IT Sectr Објављено: 2026-03-18 Време читања: 10 мин

Deadlock (узајамно блокирање) — је стање у којем два или више нити бесконачно чекају ослобађање ресурса које су заузели други учесници. Према Oracle Java Tutorials (2024), Deadlock настаје при кружном чекању, кад сваки нит држи браву потребну другом нити. Без посебних средстава детекције, Deadlock потпуно зауставља извршавање апликације без видљивих грешака.

Главно

  • Deadlock — узајамно блокирање нити, при чему свака чека ресурс који је заузео други нит
  • Четири услова Coffman (Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait) су потребни за настанак Deadlock-а
  • Deadlock се разликује од Starvation-а по томе што нити нису блокиране, већ активно чекају у кружној зависности
  • Thread Dump — основни алат за откриље кружног чекања у JVM и Android Runtime
  • Хијерархија блокада и јединствени редослед захватања ресурса — главни начин превенције узајамног блокирања

Шта је Deadlock?

Deadlock (узајамно блокирање) — је ситуација у вишенитном програмирању у којој се два или више нити трајно блокирају један другог. Сваки нит држи ресурс потребан другом нити и га не ослабађа, чекајући захват недостајућег ресурса. Као резултат, ниједан од нити не може да настави извршавање.

У мобилном развоју, Deadlock је посебно критичан јер не изазива изузетке или падове. Апликација једноставно престаје да реагује на акције корисника (ANR — Application Not Responding), а једини излаз је принудно завршавање процеса. Према подацима Google (Android Performance Patterns, 2023), око 15% ANR извештаја у Google Play Console је повезано са узајамним блокирањима у позадинским нитима.

Кључна разлика Deadlock-а од других проблема конкурентности — његова неповратност без спољашње интервенције. Нити неће самостално ослободити ресурсе, јер планер оперативног система не може принудно да повуче браву. То разликује Deadlock од Livelock-а, где су нити активне, али не обављају корисан посао.

Услови настанка Deadlock-а

1971. године, Edward G. Coffman је формулисао четири обавезна услова потребна за настанак Deadlock-а. Ако бар један од њих недостаје, узајамно блокирање је немогуће. Ови услови су познати као Coffman-ови услови и чине основу свих алгоритама за превенцију Deadlock-а.

Међусобно искључивање (Mutual Exclusion)

Ресурс може бити захваћен само једним нитом у сваком тренутку. Ако ресурс дозвољава истовремено читање од стране више нити (nа пример, ReadWriteLock у начину читања), Deadlock не настаје. Овај услов произлази из саме природе Mutex-а и брава.

Држање и чекање (Hold and Wait)

Нит држи већ захваћен ресурс и истовремено чека захват другог ресурса. Ако нит може да ослободи тренутни ресурс пре захтева за следећим (кроз двофазно закључавање), услов Hold and Wait је прекршен. У Android-у, ово се често манифестује кад нит држи браву базе података и покушава да захвати браву SharedPreferences.

Непостојање принудног преемпције (No Preemption)

Оперативни систем не може принудно да одузме браву од нити. Ресурс се ослабађа само кад га нит сам ослободи. У неким системима (nа пример, SQLite WAL начин), принудно преемпција је имплементирана на нивоу појединачних операција, што смањује ризик од Deadlock-а.

Кружно чекање (Circular Wait)

Постоји затворени ланац нити, од којих свака чека ресурс који држи следећа у ланцу. На пример, нит A држи ресурс 1 и чека ресурс 2, нит B држи ресурс 2 и чека ресурс 1. Ово је једини услов који програмер може да елиминише архитектурно — кроз хијерархију брава. Ако све нити захватају ресурсе у строго одређеном глобалном редоследу, циклус је физички немогућ.

У пракси, у Android апликацијама, Deadlock најчешће настаје због имплицитног пресекања брава различитих нивоа: браве базе података (Room), браве SharedPreferences и браве колекције у меморији. Сваком од ових брава управљају различити компоненти, и без централизованог протокола редоследа захватања, програмери намерно стварају циклусе.

Пример Deadlock-а у Kotlin коду

Размотримо класичан пример узајамног блокирања — два нита захватају браве у различитом редоследу. Ако први нит блокира ресурс A и покушава да захвати B, а други — блокира B и покушава да захвати A, настаје Deadlock.

kotlin
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun operationA() {
        synchronized(lockA) {
            Thread.sleep(50)  // симулација рада
            synchronized(lockB) {
                println("operationA је извршена")
            }
        }
    }

    fun operationB() {
        synchronized(lockB) {  // обрнути редослед!
            Thread.sleep(50)
            synchronized(lockA) {
                println("operationB је извршена")
            }
        }
    }
}

fun main() {
    val ex = DeadlockExample()
    Thread { ex.operationA() }.start()
    Thread { ex.operationB() }.start()
    // Апликација ће се завек заблокирати — Deadlock!
}

У овом примеру, operationA захвата lockA, а operationB захвата lockB. Затим сваки покушава да захвати другу браву — и оба бесконачно чекају. Програм се заблокира без поруке о грешци. Једини начин поправке је гарантовање истог редоследа захватања брава у свим методама.

Deadlock vs Starvation vs Livelock

Ова три проблема конкурентности се често мешају, али су им механизми и последице потпуно различити. Deadlock — потпуно заустављање, Starvation — бесконачно чекање ресурса, Livelock — активна неактивност. Разумевање разлика је кључно за одабир праве стратегије отклањања.

КарактеристикаDeadlockStarvationLivelock
Стање нитиБлокиране (BLOCKED)Спремне (RUNNABLE)Активне (RUNNABLE)
Обављање послаНеНеДа, али безкорисно
УзрокКружно чекањеНеправедливо планирањеНеисправно руковање конфликтом
ОткривањеThread Dump, временски лимитиПраћење напреткаБројач поновних покушаја

Starvation (гладовање) настаје кад планер константно одлага извршавање нити ниског приоритета у корист других. За разлику од Deadlock-а, нит није блокиран — спреман је за извршавање, али не добија процесорско време. У Android-у, типичан сценарио је позадинска нит ниског приоритета која се никада не извршава ако су UI и Service нити стално активне.

Livelock (активно блокирање) — ситуација у којој нити нису блокиране, али бесконачно реагују на акције један другога, не обављајући корисан посао. Класична аналогија — две особе сусрету се у ходнику и обе покушавају да уступе пут, крећући се у истом смеру. За разлику од Deadlock-а, нити у Livelock-у троше CPU, празнећи батерију уређаја.

Како открити Deadlock

Thread Dump (испис нити) — основни алат за откривање узајамних блокада у JVM и Android Runtime. При испису, JVM аутоматски анализира граф зависности између монитора и означава Deadlock циклусе. У Android Studio-у, испис нити се може добити путем Android Profiler или наредбе kill -3 PID из ADB Shell.

Аутоматско откривање Deadlock-а у току извршавања се постиже путем Watchdog тајмера. Ако нит не заврши операцију у одређеном року, watchdog покреће испис и шаље извештај у систем за извештавање падова (Firebase Crashlytics, Sentry). Према подацима Sentry (Issue Resolution Report, 2024), подешавање watchdog-а скраћује време дијагнозе Deadlock-а са недеља на неколико сати.

У фази развоја ефикасни су статички анализатор ThreadSafe од JetBrains-а и Checker Framework са модулом Lock Checker. Ови алати анализирају редослед захватања брава на нивоу изворног кода и упозоравају на потенцијалне циклусе. Додатно, препоручује се Test-Driven Deadlock Detection — стрес тестови који покрећу операције са различитим редоследом блокирања у стотинама нити.

Посебне пажње заслужује Cooperative Deadlock Detection — метода у којој нити размењују информације о захваћеним бравама кроз глобални регистар. Ако нит открије потенцијални циклус, ослабађа све ресурсе и понавља операцију. Овај приступ се користи у расподијељеним системима (Apache ZooKeeper, Google Chubby) и постепено се уводи у мобилни развој кроз библиотеке као што је Jetpack Sync.

Методе превенције узајамног блокирања

Хијерархија блокада (Lock Ordering)

Најсигурнији начин — поставити глобални редослед захватања брава у целој апликацији. Ако све нити увек прво захватају браву са мањим бројем, затим са већим, кружно чекање (услов Circular Wait) је немогуће. У великим пројектима, редослед се фиксира у документацији и проверава путем code review.

TryLock са временским лимитом

TryLock — метода блокирања која не блокира нит бесконачно, већ враћа false ако се брава не добије у одређеном року. У Javi је ово имплементирано кроз ReentrantLock.tryLock(timeout, TimeUnit), у Kotlin Coroutines — кроз Mutex.withLock са временским лимитом. У случају неуспеха, нит ослабађа све захваћене ресурсе и понавља покушај касније.

Банкарски алгоритам (Banker’s Algorithm)

Банкарски алгоритам — теоријска метода превенције Deadlock-а, коју је предложио Edsger Dijkstra. Он моделира расподјелу ресурса као банкарске транзакције: систем не додељује ресурс ако то може да доведе до несигурног стања (deadlock). У пракси, алгоритам се ретко примењује у мобилном развоју због сложености претходног познавања максималних потреба нити, али његови принципи се користе у SQLite базама података и датотечним системима.

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

Да ли Deadlock може да настане у једнонитној апликацији?

Не, за узајамно блокирање је потребно најмање два нити. У једнонитном коду, све операције се извршавају секвенцијално, па је кружно чекање немогуће. Ипак, Deadlock може настати између процеса при кориштењу брава датотека или међупроцесних семафора.

По чему се Deadlock у Kotlin Coroutines разликује од Deadlock-а у нитима?

У корутинама, Deadlock настаје на нивоу прекинутих функција (suspend) и не блокира OS нит, што га чини мање приметним. Mutex из kotlinx.coroutines је прекидајући (suspending), не блокира нит, али се корутина не извршава. За откривање користите DebugProbes из модула kotlinx-coroutines-debug.

Шта је Deadlock у SQLite-у на Android-у?

Deadlock у SQLite-у настаје кад две везе ка бази података покушавају да изврше транзакције у различитом редоследу. SQLite открива такве ситуације и враћа код грешке SQLITE_BUSY или SQLITE_LOCKED. У Android-у се препоручује кориштење Room-а са јединственом инстанцом базе података и транзакцијама кроз @Transaction, што елиминише међувезни Deadlock.

Како Android открива Deadlock?

Android Runtime има уграђен детектор Deadlock-а који се покреће при генерисању ANR-а (Application Not Responding). Систем анализира Thread Dump свих нити апликације и означава узајамне блокаде. Резултат је доступан у /data/anr/traces.txt и Google Play Console у секцији ANR Reports.

Шта урадити ако је Deadlock пронађен у производњи?

Прво добијте Thread Dump свих нити апликације. Анализирајте које браве свака нит држи и које покушава да захвати. Имплементирајте Watchdog тајмер са аутоматским исписом по прекорачењу временског лимита. Након поправке, додајте lint правило ThreadSafety у CI пиплајн за превенцију рецидива.

Закључак

  • Deadlock — узајамно блокирање, при којем нити бесконачно чекају ресурсе које су заузели један другом
  • Четири Coffman услова (Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait) су потребни за настанак Deadlock-а
  • Thread Dump — стандардна метода за откривање узајамних блокада у JVM и Android Runtime
  • Хијерархија блокада са јединственим глобалним редоследом потпуно елиминише услов кружног чекања
  • TryLock са временским лимитом спречава бесконачно чекање и омогућује нити да правилно обради недоступност ресурса
  • Deadlock vs Starvation — код Deadlock-а нити су блокиране, код Starvation-а спремне за извршавање али не добијају CPU
  • Watchdog тајмери и статички анализатори (ThreadSafe, Checker Framework) — основна заштита од Deadlock-а у CI/CD

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

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

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