Mutex (ömsesidig uteslutning) — är en synkroniseringsprimitiv som garanterar att endast en tråd kan utföra den kritiska sektionen kod vid varje given tidpunkt. Enligt Microsoft Docs (Synchronization Objects, 2024) är grundprincipen för Mutex ägande: tråden som har tagit Mutex blir dess ägare och frigör den endast när den lämnar den kritiska sektionen. Mutex — ett grundläggande verktyg för att förhindra Race Condition och säkerställa dataintegritet i flertrådade applikationer.
Huvudpunkter
Mutex (förkortning av Mutual Exclusion — ömsesidig uteslutning) — är ett synkroniseringsobjekt som hanterar åtkomst till en delad resurs i en flertrådad miljö. När en tråd går in i den kritiska sektionen tar den Mutex. Om en annan tråd försöker ta samma Mutex, övergår den till vänteläge tills låset frigörs av den första tråden.
Arkitekturen för Mutex går tillbaka till operativsystemet THE, utvecklat av Edsger Dijkstra 1965. Det var Dijkstra som introducerade konceptet med semaforer, från vilka Mutex senare avskiljdes som ett specialfall — binär semafor med stöd för ägande. Moderna operativsystem (Linux, Windows, Android) implementerar Mutex på kärnnivå, vilket säkerställer korrekt synkronisering även mellan olika processer.
Den viktigaste egenskapen hos Mutex är ownership (ägande). Endast tråden som tog mutexen kan frigöra den. Detta skiljer Mutex från den binära semaforen, där vilken tråd som helst kan utföra en signal (V-operation). Ägande förhindrar oavsiktlig frigöring av låset av en annan tråd, vilket gör Mutex säkrare för typiska synkroniseringsscenarier inom mobilutveckling. Enligt Android Developer Docs (Processes and Threads, 2024) kan användning av Mutex istället för synchronized öka prestandan med 30% vid hög konkurrens.
Mutex befinner sig i ett av två tillstånd: låst (locked) — taget av en tråd; ledig (unlocked) — inte taget. Två grundläggande operationer — lock() (ta) och unlock() (frigöra). Om Mutex redan är taget blockeras tråden som anropar lock() tills frigöring. I JVM går den blockerade tråden till tillståndet BLOCKED och förbrukar inte CPU.
När Mutex frigörs väljer systemet vilken av de väntande trådarna som får låset. Vid orättvis (non-fair) planering kan valet falla på tråden som nyss frigjorde mutexen — detta ökar genomströmningen men kan leda till Starvation (svält). En rättvis (fair) planerare använder FIFO-kö: den första väntande tråden får låset först. ReentrantLock(true) implementerar precis denna mekanism.
De flesta Mutex-implementationer i Java/Kotlin stöder rekursivt (reentrant) tagande. Om en tråd redan äger Mutex och anropar lock() igen, är operationen framgångsrik — Mutex blockerar inte sig själv. Rekursionsräknaren ökar och tråden måste anropa unlock() lika många gånger som lock(). Detta är viktigt för rekursiva anrop och nästlade kritiska sektioner.
Låt oss betrakta en typisk uppgift — att skydda en delad räknare mot Race Condition med ReentrantLock (klassisk Mutex i Java/Kotlin). Utan Mutex skulle koden ge ett felaktigt resultat; med Mutex ökar alla 1000 trådar garanterat räknarens värde.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // kritisk sektion
} finally {
mutex.unlock() // obligatorisk finally
}
}
fun getCount(): Int {
mutex.lock()
try {
return count
} finally {
mutex.unlock()
}
}
}
fun main() = runBlocking {
val counter = MutexCounter()
val jobs = List(1000) {
launch(Dispatchers.Default) {
counter.increment()
}
}
jobs.forEach { it.join() }
println(counter.getCount()) // Alltid 1000
}
Lägg märke till finally-blocket — obligatoriskt mönster vid arbete med Mutex. Om ett undantag uppstår inom den kritiska sektionen, anropas inte unlock(), och Mutex förblir låst för alltid — detta leder till Deadlock. Finally-blocket garanterar frigöring av Mutex oavsett utfall av sektionens exekvering.
Ett alternativt tillvägagångssätt i Kotlin — användning av tilläggsfunktionen withLock, som automatiskt hanterar lock/unlock med finally.
fun increment() {
mutex.withLock { // lock + try/finally automatiskt
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Dessa tre synkroniseringsmekanismer förväxlas ofta, även om de har olika egenskaper och tillämpningsområden. Mutex — binär, med ägande. Semafor — räknare av tillstånd, utan ägande. Monitor — mekanism på hög nivå som kombinerar Mutex med villkorsvariabler (condition variables). Att förstå skillnaderna är avgörande för att välja rätt verktyg för en specifik uppgift.
| Parameter | Mutex | Semafor | Monitor |
|---|---|---|---|
| Typ | Binär (0/1) | Räkningsbar (0..N) | Binär + villkor |
| Ägande | Endast ägaren kan unlock:a | Vilken tråd som helst kan signal:a | Endast ägaren |
| Rekursivitet | Vanligtvis ja (reentrant) | Nej | Ja |
| Villkorlig väntan | Nej (behöver Condition) | Nej | Inbyggd (wait/notify) |
| Exempel i Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
När välja Mutex: man måste skydda en resurs från samtidig åtkomst — till exempel en delad samling, fil eller räknare. När välja Semafor — man måste begränsa antalet samtidiga åtkomster till en resurs pool, till exempel en databaskopplingspool med 5 anslutningar. När välja Monitor — synkronisering med villkorlig väntan behövs, till exempel en producent-konsument kö via wait/notify. I modern Android-utveckling ersätts synchronized ofta med ReentrantLock eller kotlinx.coroutines Mutex.
Det vanligaste misstaget — avsaknad av finally-block för att anropa unlock(). Om ett undantag uppstår i den kritiska sektionen förblir Mutex låst och andra trådar väntar för evigt. Även om du är säker på att undantag är omöjliga — använd alltid try/finally eller withLock. Detta är principen för defensive programming, särskilt viktig inom mobilutveckling, där undantag kan uppstå på grund av minnesbrist eller Configuration Changes.
När flera Mutex används i en applikation är det avgörande att fastställa en enhetlig ordning för deras tagande. Om Tråd A tar M1 → M2 och Tråd B tar M2 → M1 uppstår Deadlock. I stora projekt (över 50 tusen rader kod) dokumenteras låsordningen i arkitekturbeslutet och kontrolleras av linters. Verktyget Lock Checker i IntelliJ IDEA upptäcker automatiskt inkonsekvent låsordning.
Att hålla Mutex längre än 1-2 millisekunder — ett tecken på felaktig design. Den kritiska sektionen bör endast innehålla minimalt nödvändiga operationer. Nätverksförfrågningar, fil-I/O och komplexa beräkningar bör utföras utanför det låsta blocket. I Android leder långvarigt hållande av lås i UI-tråden till bildrutehopp (jank) och ANR. Använd ReadWriteLock om den kritiska sektionen huvudsakligen består av läsoperationer.
Biblioteket kotlinx.coroutines tillhandahåller en egen implementering av Mutex, som fundamentalt skiljer sig från klassisk ReentrantLock. Huvudskillnaden — suspending Mutex blockerar inte OS-tråden, utan pausar koroutinen tills låset frigörs. Detta innebär att tråden kan utföra andra koroutiner medan den aktuella väntar på Mutex.
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class CoroutineCounter {
private val mutex = Mutex()
private var count = 0
suspend fun increment() {
mutex.withLock { // suspending — blockerar inte tråden
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Viktiga egenskaper hos kotlinx Mutex: icke-reentrant — till skillnad från ReentrantLock kan en koroutin inte återta en Mutex som den redan äger. Om nödvändigt, använd Semaphore(1) istället för Mutex. Dessutom är Mutex från kotlinx.coroutines icke-blockerande: den använder pausning via suspend, vilket gör att pooltråden inte blockeras.
I praktiken är suspending Mutex att föredra framför klassisk ReentrantLock i koroutinkod av två skäl: skalbarhet — en koroutin väntar på Mutex medan tråden betjänar andra koroutiner, vilket ökar systemets genomströmning; frånvaro av BlockedThread — inga resurser slösas på att lagra stacken för den blockerade tråden. Enligt JetBrains (Kotlin Coroutines Guide, 2024) ökar användning av suspending Mutex genomströmningen med 40% vid 100+ koroutiner.
Vanliga frågor
Ägande (ownership) — den fundamentala skillnaden. Mutex kommer ihåg vilken tråd som tog den och endast den tråden kan frigöra den. En binär semafor (Semaphore(1)) har ingen ägare — vilken tråd som helst kan utföra release(). Därför är Mutex säkrare: en annan tråd kan inte oavsiktligt frigöra någon annans lås, medan en semafor kan.
synchronized är enklare och kortare — använd det för enkla kritiska sektioner utan timeout och utan rättvisekontroll. Använd ReentrantLock när TryLock med timeout, fair-planering, Condition Variables eller avbrott av väntande tråd (lockInterruptibly) behövs. För koroutiner, använd alltid kotlinx.coroutines.sync.Mutex.
Spinlock — är ett lås där tråden inte sover utan i en slinga (spin) kontrollerar låsets tillstånd. Spinlock förbrukar CPU men växlar inte kontext, vilket gör den fördelaktig för korta kritiska sektioner (upp till 10 instruktioner). Mutex för tråden till tillståndet BLOCKED, vilket är 10-50 mikrosekunder dyrare på grund av kontextväxling, men förbrukar inte CPU.
På Linux kärnnivå är Mutex implementerad genom futex (fast userspace mutex). Tråden försöker först ta låset i userspace via en atomär CAS-instruktion (Compare-And-Swap). Om Mutex är ledig — sker tagandet utan syscall. Om den är upptagen — gör tråden syscall futex(FUTEX_WAIT) och somnar. Vid frigöring väcker syscall futex(FUTEX_WAKE) en väntande tråd.
Ja, det finns inter-process Mutex (inter-process mutex). I Windows är det Named Mutex, i Linux — pthread_mutexattr_setpshared med attributet PTHREAD_PROCESS_SHARED. I Android stöder Bionic libc också inter-process Mutex via filbeskrivare. Inter-process Mutex används för synkronisering mellan olika applikationer eller mellan en process och dess underprocesser.
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å