Livelock (активно блокирање) — је ситуација у вишенитном програмирању када нити нису блокиране, али бесконачно реагују на акције једна друге не обављајући користан посао. Према Baeldung (Java Concurrency Guide, 2024), при Livelock нити стално мењају стање као одговор на стање суседних нити, али ниједна не постиже циљ. За разлику од Deadlock, Livelock троши 100% CPU, што брзо празни батерију мобилног уређаја.
Главно
Livelock (активно блокирање) — је ситуација у вишенитном систему when нити нису блокиране али не обављају ни користан посао. Свака нит открива да не може да настави рад и покушава да то поправи, али њене акције изазивају исту реакцију код других нити. Као резултат, систем бесконачно прелази између стања без постизања напретка.
Класична аналогија Livelock — две особе се срећу у уском ходнику. Свака покушава да уступи пут склањајући се у страну, али обе истовремено раде исти покрет и поново су једна испред друге. Оне не стоје на месту (то би био Deadlock), већ се активно крећу, али се и даље не могу разићи. У програмирању, ово одговара нитима које стално ослобађају и поново заузимају ресурсе.
У мобилном развоју Livelock је посебно опасан јер је невидљив за корисника: апликација не замрзава, интерфејс се не блокира, али батерија се празни 2-3 пута брже због 100% оптерећења CPU позадинским нитима. Према тестовима Google (Android Battery Optimization, 2023), Livelock у позадинском Service може смањити време рада уређаја на батерију за 40%.
Livelock настаје када више нити користи исту стратегију реакције на конфликт. Ако нит A не може да заузме ресурс и ослобађа свој тренутни ресурс, а нит B ради исто истовремено, обе понављају циклус — и ситуација се понавља бесконачно. Ово је посебно карактеристично за алгоритме са TryLock и аутоматским ослобађањем при неуспеху.
Када нити користе фиксно кашњење пре поновног покушаја, могу ући у синхрони циклус. Ако обе нити чекају исто време, поново ће истовремено покушати да заузму ресурс и истовремено га ослободити. Проблем се решава употребом exponential backoff са насумичном компонентом (jitter), као у алгоритму CSMA/CD у Ethernet.
У мобилном развоју Livelock често настаје при неправилној имплементацији редова задатака. На пример, када радни нит заврши обраду поруке, али због логике приоритизације стално преноси контролу другом раднику који ради исто. Такве ситуације су типичне за прилагођене ThreadPoolExecutor-е са нестандардном политиком RejectedExecutionHandler.
Размотримо ситуацију када две нити користе TryLock и ослобађају ресурс при неуспеху. Активно блокирање настаје јер обе нити примењују исту логику и синхроно понављају покушаје.
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.TimeUnit
class LivelockWorker(private val name: String,
private val lock1: ReentrantLock,
private val lock2: ReentrantLock) {
fun execute() {
while (true) {
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
println("$name — извршено!")
lock2.unlock()
lock1.unlock()
return
} else {
lock1.unlock() // ослобађамо и понављамо
}
}
Thread.sleep(50) // фиксно кашњење — кључни фактор Livelock
}
}
}
Ако се две инстанце LivelockWorker покрену са различитим редоследом заузимања lock1 и lock2, оне ће ући у активно блокирање. Свака ће заузети први ресурс, не добити други, ослободити први, чекати 50 ms и понављати — бесконачно, трошећи CPU. Поправка — додати насумичну компоненту кашњењу (jitter) и ограничити број поновних покушаја.
Поправљена верзија користи exponential backoff са насумичним jitter. Након сваког неуспешног покушаја време чекања се повећава уз додавање насумичног множиоца, што разбија синхронизацију између нити.
fun executeWithBackoff() {
var delay = 10L
var attempts = 0
while (attempts < 5) {
if (lock1.tryLock(delay, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(delay, TimeUnit.MILLISECONDS)) {
println("Успех!")
lock2.unlock(); lock1.unlock()
return
}
lock1.unlock()
}
delay = (delay * 2 + (0..50).random())
attempts++
}
println("Није успело после 5 покушаја")
}
Упркос спољашњој сличности, Livelock и Deadlock имају принципијелно различите механизме и последице. Код Deadlock нити су блокиране и не троше CPU — апликација се једноставно замрзне. Код Livelock нити су активне, троше 100% CPU, али не обављају користан посао. Избор стратегије уклањања зависи од правилне идентификације типа блокирања.
| Параметар | Deadlock | Livelock |
|---|---|---|
| Стање нити | BLOCKED / WAITING | RUNNABLE |
| Потрошња CPU | Минимална | Висока (90-100%) |
| Потрошња батерије | Ниска | Висока |
| Откривање | Thread Dump | CPU Profiler + визуелна анализа |
| Типичан узрок | Различит редослед заузимања блокирања | Иста стратегија реакције на конфликт |
| Поправка | Хијерархија блокирања | Retry limit + exponential backoff |
У мобилном развоју практична разлика је огромна. Deadlock доводи до ANR и поновног покретања апликације — открива се и пријављује кроз Google Play Console. Livelock остаје непримећен: апликација изгледа као да ради, али батерија се празни за сат времена, и корисник једноставно брише апликацију. Према Firebase Analytics (App Retention Report, 2024), 68% корисника брише апликацију ако претерано троши батерију у позадини.
Откривање Livelock је теже од Deadlock, јер систем не даје очигледне сигнале — нема изузетака, нема ANR, нема порука о грешкама. Главни метод дијагностике — CPU Profiler у Android Studio. Ако је нит стално у стању RUNNABLE, али не обавља корисне улазно-излазне операције или прорачуне — то је сумња на Livelock.
Додатни знак — аномална потрошња батерије при мировању апликације. Android Battery Historian (алат из Android SDK) гради графиконе потрошње енергије по компонентама. Ако се CPU Wakelock одржава без видљивог разлога — вреди покренути Method Tracing и анализирати стек позива сумњивих нити.
На нивоу кода помаже логирање поновних покушаја са навођењем threadId и времена. Ако лог показује хиљаде поновних покушаја у секунди без иједног успеха — то је Livelock. Препоручује се имплементација Hystrix-сличног circuit breaker или бројача retry са прагом активирања који при прекорачењу искључује операцију и обавештава програмера кроз Crashlytics.
Најједноставнији и најпоузданији начин — ограничити број покушаја заузимања ресурса. Ако после N покушаја операција није успела, нит прелази у стање грешке и обавештава корисника. N се бира емпиријски: за мобилне апликације обично 3-5 покушаја. Ово потпуно елиминише бесконачни Livelock по цену ретких лажних активирања при високом оптерећењу.
Уместо фиксног кашњења између покушаја користи се експоненцијално растућа пауза са насумичном компонентом. Формула: delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter). Овај приступ не само да разбија синхронизацију нити, већ и смањује укупно оптерећење система при високој конкуренцији. Користи се у алгоритмима мрежних протокола и препоручен је од Google за логику поновних покушаја Firebase Realtime Database.
Додељивање различитих стратегија различитим нитима уклања сам узрок Livelock — исту реакцију на конфликт. На пример, нит са високим приоритетом добија ресурс без ослобађања, а нископриоритетна ослобађа и чека. У мобилном развоју UI нит може имати приоритет при заузимању блокирања, а позадинске радничке нити могу користити TryLock са временским ограничењем.
У неким архитектурама Livelock се спречава на нивоу дизајна: ослобађање ресурса само у једном смеру. На пример, ако нит A увек преноси контролу ниту B кроз фиксни канал (Channel), а B никада не покушава да врати контролу A — циклус реакције је немогућ. Pipeline архитектура са једносмерним фазама обраде у Android CameraX и MediaPipe потпуно елиминише Livelock између суседних фаза.
Често постављана питања
Бесконачна петља не зависи од спољашњих фактора и понавља једну операцију без интеракције са другим нитима. Livelock је увек реакција на акције других нити: нит мења понашање као одговор на стање суседних нити, стварајући затворену повратну спрегу. Thread Dump у случају Livelock показује стално пребацивање контекста.
У базама података Livelock настаје када се трансакција стално одлаже због блокирања од стране других трансакција. На пример, DBMS користи алгоритам wait-die: ако трансакција са краћим временом старта конфликтује са новијом, бива поништена и поново покренута, али сваки пут наилази на исти конфликт. Решава се помоћу randomized restart delay.
У неким системима Livelock је пожељнији од Deadlock јер нити остају активне и могу да открију проблем. На пример, у алгоритмима оптимистичког блокирања (optimistic locking) livelock-слично понашање је дозвољено ако retry limit гарантује коначни завршетак. Ово је компромис између перформанси и гаранције напретка.
Livelock је изузетно тешко репродуковати у тестовима јер захтева тачно подударање тајминга нити. Јединични тестови се извршавају детерминистички и ретко откривају активно блокирање. Препоручује се Stress Testing са вишеструким покретањем под оптерећењем и праћењем потрошње CPU у профилеру.
На серверу Livelock доводи до деградације перформанси и timeout-а, али сервер се скалира хоризонтално. На Android-у Livelock празни батерију и прегрева уређај, стварајући лошије корисничко искуство. Поред тога, на мобилним уређајима је ограничен број CPU језгара, па Livelock брже доводи до неспособности целог система.
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође