Livelock (aktiv blockering) — är en situation inom flertrådig programmering där trådar inte är blockerade, men oändligt reagerar på varandras handlingar utan att utföra nyttigt arbete. Enligt Baeldung (Java Concurrency Guide, 2024), vid Livelock ändrar trådar ständigt tillstånd som svar på tillståndet hos angränsande trådar, men ingen når målet. Till skillnad från Deadlock, förbrukar Livelock 100% CPU, vilket snabbt tömmer batteriet på en mobil enhet.
Huvudpunkter
Livelock (aktiv blockering) — är en situation i ett flertrådigt system där trådar inte är blockerade men inte heller utför nyttigt arbete. Varje tråd upptäcker att den inte kan fortsätta arbetet och försöker åtgärda det, men dess handlingar orsakar samma reaktion hos andra trådar. Som ett resultat växlar systemet oändligt mellan tillstånd utan att göra framsteg.
Den klassiska analogin för Livelock — två personer möts i en smal korridor. Båda försöker lämna väg genom att kliva åt sidan, men båda gör samtidigt samma rörelse och står återigen mitt emot varandra. De står inte stilla (det skulle vara Deadlock), utan rör sig aktivt men kan ändå inte komma förbi varandra. Inom programmering motsvarar detta trådar som ständigt frigör och återtar resurser.
I mobilutveckling är Livelock särskilt farligt eftersom det är osynligt för användaren: applikationen fryser inte, gränssnittet blockeras inte, men batteriet töms 2-3 gånger snabbare på grund av 100% CPU-belastning från bakgrundstrådar. Enligt Google-tester (Android Battery Optimization, 2023) kan Livelock i en bakgrundstjänst minska enhetens batteritid med 40%.
Livelock uppstår när flera trådar använder samma strategi för konfliktreaktion. Om Tråd A inte kan fånga en resurs och frigör sin nuvarande resurs, och Tråd B gör samma sak samtidigt, upprepar båda cykeln — och situationen upprepas oändligt. Detta är särskilt karakteristiskt för algoritmer med TryLock och automatisk frigöring vid misslyckande.
När trådar använder fast fördröjning före ett förnyat försök, kan de hamna i en synkron cykel. Om båda trådarna väntar lika länge kommer de samtidigt att försöka fånga resursen och samtidigt frigöra den. Problemet löses med exponential backoff med en slumpmässig komponent (jitter), som i CSMA/CD-algoritmen i Ethernet.
I mobilutveckling uppstår Livelock ofta vid felaktig implementering av uppgiftsköer. Till exempel när en arbetstråd slutför bearbetning av ett meddelande, men på grund av prioriteringslogik ständigt överför kontrollen till en annan arbetstråd som gör samma sak. Sådana situationer är typiska för anpassade ThreadPoolExecutor med icke-standard policy för RejectedExecutionHandler.
Låt oss undersöka en situation där två trådar använder TryLock och frigör resursen vid misslyckande. Aktiv blockering uppstår eftersom båda trådarna tillämpar samma logik och synkront upprepar försöken.
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 — utfört!")
lock2.unlock()
lock1.unlock()
return
} else {
lock1.unlock() // frigör och upprepar
}
}
Thread.sleep(50) // fast fördröjning — nyckelfaktor för Livelock
}
}
}
Om två instanser av LivelockWorker körs med olika ordning för att fånga lock1 och lock2, hamnar de i aktiv blockering. Var och en fångar den första resursen, får inte den andra, frigör den första, väntar 50 ms och upprepar — oändligt, förbrukande CPU. Åtgärd — lägga till en slumpmässig komponent till fördröjningen (jitter) och begränsa antalet förnyade försök.
Den korrigerade versionen använder exponential backoff med slumpmässig jitter. Efter varje misslyckat försök ökar väntetiden med tillägg av en slumpmässig multiplikator, vilket bryter synkroniseringen mellan trådar.
fun executeWithBackoff() {
var delay = 10L
var attempts = 0
while (attempts < 5) {
if (lock1.tryLock(delay, TimeUnit.MILLISECONDS)) {
if (lock2.tryLock(delay, TimeUnit.MILLISECONDS)) {
println("Framgång!")
lock2.unlock(); lock1.unlock()
return
}
lock1.unlock()
}
delay = (delay * 2 + (0..50).random())
attempts++
}
println("Misslyckades efter 5 försök")
}
Trots yttre likhet har Livelock och Deadlock fundamentalt olika mekanismer och konsekvenser. Vid Deadlock är trådar blockerade och förbrukar inte CPU — applikationen fryser helt enkelt. Vid Livelock är trådar aktiva, förbrukar 100% CPU, men utför inte nyttigt arbete. Valet av elimineringsstrategi beror på korrekt bestämning av blockeringstypen.
| Parameter | Deadlock | Livelock |
|---|---|---|
| Trådstatus | BLOCKED / WAITING | RUNNABLE |
| CPU-förbrukning | Minimal | Hög (90-100%) |
| Batteriförbrukning | Låg | Hög |
| Upptäckt | Thread Dump | CPU Profiler + visuell analys |
| Typisk orsak | Olika ordning för att fånga lås | Samma strategi för konfliktreaktion |
| Åtgärd | Låshierarki | Retry limit + exponential backoff |
I mobilutveckling är den praktiska skillnaden enorm. Deadlock leder till ANR och omstart av applikationen — det upptäcks och rapporteras via Google Play Console. Livelock förblir oupptäckt: applikationen verkar fungera, men batteriet töms på en timme och användaren tar helt enkelt bort applikationen. Enligt Firebase Analytics (App Retention Report, 2024) tar 68% av användarna bort applikationen om den förbrukar batteri överdrivet mycket i bakgrunden.
Upptäckt av Livelock är svårare än Deadlock eftersom systemet inte ger tydliga signaler — det finns inga undantag, inget ANR, inga felmeddelanden. Den huvudsakliga diagnosmetoden — CPU Profiler i Android Studio. Om en tråd ständigt är i RUNNABLE-tillstånd men inte utför användbara inmatnings-utmatningsoperationer eller beräkningar — är det en misstanke om Livelock.
Ett ytterligare tecken — onormal batteriförbrukning när applikationen är inaktiv. Android Battery Historian (ett verktyg från Android SDK) bygger grafer över energiförbrukning per komponent. Om CPU Wakelock upprätthålls utan synlig anledning — bör du köra Method Tracing och analysera anropsstacken för misstänkta trådar.
På kodnivå hjälper loggning av förnyade försök med threadId och tid. Om loggen visar tusentals förnyade försök per sekund utan någon framgång — är detta Livelock. Rekommendationen är att implementera en Hystrix-liknande circuit breaker eller en räknare för retry med en tröskel som vid överskridande inaktiverar operationen och meddelar utvecklaren via Crashlytics.
Det enklaste och mest pålitliga sättet — begränsa antalet försök att fånga en resurs. Om operationen inte lyckas efter N försök, går tråden till feltillstånd och meddelar användaren. N väljs empiriskt: för mobilapplikationer vanligtvis 3-5 försök. Detta eliminerar helt oändlig Livelock till priset av sällsynta falska utlösningar vid hög belastning.
Istället för fast fördröjning mellan försök används en exponentiellt ökande paus med en slumpmässig komponent. Formel: delay = min(baseDelay * 2^attempt, maxDelay) + random(0, jitter). Detta tillvägagångssätt bryter inte bara trådsynkronisering utan minskar också den totala systembelastningen vid hög konkurrens. Det används i algoritmer för nätverksprotokoll och rekommenderas av Google för logik för förnyade försök i Firebase Realtime Database.
Att tilldela olika strategier till olika trådar eliminerar själva orsaken till Livelock — samma reaktion på konflikt. Till exempel får en tråd med hög prioritet resursen utan frigöring, medan en lågprioriterad tråd frigör och väntar. I mobilutveckling kan UI-tråden ha prioritet vid infångning av lås, och bakgrundsarbetstrådar kan använda TryLock med timeout.
I vissa arkitekturer förhindras Livelock på designnivå: frigöring av resurser endast i en riktning. Till exempel, om Tråd A alltid överför kontrollen till Tråd B via en fast kanal (Channel), och B aldrig försöker återlämna kontrollen till A — är en reaktionscykel omöjlig. Pipeline-arkitektur med enkelriktade bearbetningssteg i Android CameraX och MediaPipe eliminerar helt Livelock mellan angränsande steg.
Vanliga frågor
Oändlig loop är inte beroende av externa faktorer och upprepar en operation utan interaktion med andra trådar. Livelock är alltid en reaktion på andra trådars handlingar: tråden ändrar beteende som svar på tillståndet hos angränsande trådar, vilket skapar en sluten återkoppling. Thread Dump vid Livelock visar kontextomkoppling hela tiden.
I databaser uppstår Livelock när en transaktion ständigt skjuts upp på grund av lås från andra transaktioner. Till exempel använder DBMS algoritmen wait-die: om en transaktion med kortare starttid konflikterar med en nyare, rullas den tillbaka och startas om, men varje gång hamnar i samma konflikt. Det löses med randomized restart delay.
I vissa system är Livelock att föredra framför Deadlock eftersom trådar förblir aktiva och kan upptäcka problemet. Till exempel i algoritmer för optimistisk låsning (optimistic locking) är livelock-liknande beteende tillåtet om retry limit garanterar slutlig slutföring. Detta är en kompromiss mellan prestanda och garantin för framsteg.
Livelock är extremt svårt att återskapa i tester eftersom det kräver exakt sammanträffande av trådtimings. Enhetstester körs deterministiskt och upptäcker sällan aktiv blockering. Rekommendationen är att använda Stress Testing med flera körningar under belastning och övervakning av CPU-förbrukning i profileraren.
På servern leder Livelock till prestandaförsämring och timeouter, men servern skalas horisontellt. På Android töms batteriet och enheten överhettas av Livelock, vilket skapar en sämre användarupplevelse. Dessutom är antalet CPU-kärnor på mobila enheter begränsat, så Livelock leder snabbare till att hela systemet blir oanvändbart.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också