Mutex (wederzijdse uitsluiting) — is een synchronisatieprimitief die garandeert dat slechts één thread tegelijk een kritieke sectie code kan uitvoeren. Volgens Microsoft Docs (Synchronization Objects, 2024), is het basisprincipe van Mutex eigendom: de thread die Mutex heeft verworven, wordt de eigenaar en geeft deze alleen vrij bij het verlaten van de kritieke sectie. Mutex — een fundamenteel hulpmiddel voor het voorkomen van Race Condition en het waarborgen van gegevensintegriteit in multithreaded applicaties.
Belangrijkste punten
Mutex (afkorting van Mutual Exclusion — wederzijdse uitsluiting) — is een synchronisatieobject dat de toegang tot een gedeelde bron in een multithreaded omgeving beheert. Wanneer een thread een kritieke sectie binnengaat, verwerft hij de Mutex. Als een andere thread dezelfde Mutex probeert te verwerven, wordt deze in een wachttoestand geplaatst totdat de vergrendeling door de eerste thread wordt vrijgegeven.
De architectuur van Mutex gaat terug naar het besturingssysteem THE, ontwikkeld door Edsger Dijkstra in 1965. Dijkstra introduceerde het concept van semaforen, waaruit later Mutex als een speciaal geval werd afgescheiden — een binaire semafoor met ondersteuning voor eigendom. Moderne besturingssystemen (Linux, Windows, Android) implementeren Mutex op kernelniveau, wat correcte synchronisatie zelfs tussen verschillende processen garandeert.
De belangrijkste eigenschap van Mutex is ownership (eigendom). Alleen de thread die de mutex heeft verworven, kan deze vrijgeven. Dit onderscheidt Mutex van een binaire semafoor, waar elke thread een signaal (V-operatie) kan uitvoeren. Eigendom voorkomt dat een andere thread per ongeluk de vergrendeling vrijgeeft, waardoor Mutex veiliger is voor typische synchronisatiescenario's in mobiele ontwikkeling. Volgens Android Developer Docs (Processes and Threads, 2024) kan het gebruik van Mutex in plaats van synchronized de prestaties met 30% verbeteren bij hoge concurrentie.
Mutex bevindt zich in een van twee toestanden: vergrendeld (locked) — verworven door een thread; vrij (unlocked) — niet verworven. Twee basisoperaties — lock() (verwerven) en unlock() (vrijgeven). Als Mutex al is verworven, wordt de thread die lock() aanroept geblokkeerd tot het vrijkomen. In de JVM gaat de geblokkeerde thread naar de status BLOCKED en verbruikt geen CPU.
Wanneer Mutex wordt vrijgegeven, kiest het systeem welke van de wachtende threads de vergrendeling krijgt. Bij oneerlijke (non-fair) planning kan de keuze vallen op de thread die net de mutex heeft vrijgegeven — dit verhoogt de doorvoer, maar kan leiden tot Starvation (uithongering). Een eerlijke (fair) planner gebruikt een FIFO-wachtrij: de eerste wachtende thread krijgt als eerste de vergrendeling. ReentrantLock(true) implementeert precies dit mechanisme.
De meeste Mutex-implementaties in Java/Kotlin ondersteunen recursief (reentrant) verwerven. Als een thread al eigenaar is van Mutex en opnieuw lock() aanroept, is de operatie succesvol — Mutex blokkeert zichzelf niet. De recursieteller wordt verhoogd en de thread moet unlock() even vaak aanroepen als lock(). Dit is belangrijk voor recursieve aanroepen en geneste kritieke secties.
Laten we een typische taak bekijken — het beschermen van een gedeelde teller tegen Race Condition met ReentrantLock (klassieke Mutex in Java/Kotlin). Zonder Mutex zou de code een onjuist resultaat geven; met Mutex verhogen alle 1000 threads gegarandeerd de waarde van de teller.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // kritieke sectie
} finally {
mutex.unlock() // verplichte 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()) // Altijd 1000
}
Let op het finally-blok — een verplicht patroon bij het werken met Mutex. Als er binnen de kritieke sectie een uitzondering optreedt, wordt unlock() niet aangeroepen en blijft Mutex voor altijd vergrendeld — dit leidt tot Deadlock. Het finally-blok garandeert het vrijgeven van Mutex bij elke uitkomst van de sectie-uitvoering.
Een alternatieve benadering in Kotlin — het gebruik van de extensiefunctie withLock, die automatisch lock/unlock met finally afhandelt.
fun increment() {
mutex.withLock { // lock + try/finally automatisch
count++
}
}
fun getCount(): Int = mutex.withLock { count }
Deze drie synchronisatiemechanismen worden vaak verward, hoewel ze verschillende eigenschappen en toepassingsgebieden hebben. Mutex — binair, met eigendom. Semaphore — teller van toestemmingen, zonder eigendom. Monitor — een mechanisme op hoog niveau dat Mutex combineert met conditionele variabelen (condition variables). Inzicht in de verschillen is cruciaal voor het kiezen van het juiste gereedschap voor een specifieke taak.
| Parameter | Mutex | Semaphore | Monitor |
|---|---|---|---|
| Type | Binair (0/1) | Telbaar (0..N) | Binair + voorwaarden |
| Eigendom | Alleen eigenaar kan unlock | Elke thread kan signal | Alleen eigenaar |
| Recursiviteit | Meestal ja (reentrant) | Nee | Ja |
| Conditioneel wachten | Nee (Condition nodig) | Nee | Ingebouwd (wait/notify) |
| Voorbeeld in Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
Wanneer Mutex kiezen: een bron beschermen tegen gelijktijdige toegang — bijvoorbeeld een gedeelde collectie, bestand of teller. Wanneer Semaphore kiezen — het aantal gelijktijdige toegangen tot een bronnenpool beperken, bijvoorbeeld een databaseverbindingspool met 5 verbindingen. Wanneer Monitor kiezen — synchronisatie met conditioneel wachten is nodig, bijvoorbeeld een producer-consumer wachtrij via wait/notify. In moderne Android-ontwikkeling wordt synchronized vaak vervangen door ReentrantLock of kotlinx.coroutines Mutex.
De meest voorkomende fout — ontbreken van een finally-blok voor het aanroepen van unlock(). Als er in de kritieke sectie een uitzondering optreedt, blijft Mutex vergrendeld en wachten andere threads voor altijd. Zelfs als u zeker weet dat uitzonderingen onmogelijk zijn — gebruik altijd try/finally of withLock. Dit is het principe van defensive programming, vooral belangrijk in mobiele ontwikkeling, waar uitzonderingen kunnen optreden door geheugentekort of Configuration Changes.
Wanneer in een applicatie meerdere Mutexen worden gebruikt, is het cruciaal om een uniforme volgorde van verwerving vast te stellen. Als Thread A M1 → M2 verwerft en Thread B M2 → M1 verwerft, ontstaat Deadlock. In grote projecten (meer dan 50 duizend regels code) wordt de vergrendelingsvolgorde gedocumenteerd in het architectuurbesluit en gecontroleerd door linters. De Lock Checker-tool in IntelliJ IDEA detecteert automatisch inconsistente vergrendelingsvolgorde.
Het vasthouden van Mutex langer dan 1-2 milliseconden — een teken van onjuist ontwerp. De kritieke sectie mag alleen minimaal noodzakelijke operaties bevatten. Netwerkverzoeken, bestands-I/O en complexe berekeningen moeten buiten het vergrendelde blok worden uitgevoerd. In Android leidt langdurig vasthouden van een vergrendeling in de UI-thread tot frame-overslaan (jank) en ANR. Gebruik ReadWriteLock als de kritieke sectie voornamelijk uit leesoperaties bestaat.
De bibliotheek kotlinx.coroutines biedt een eigen implementatie van Mutex, die fundamenteel verschilt van klassieke ReentrantLock. Het belangrijkste verschil — suspending Mutex blokkeert de OS-thread niet, maar schort de coroutine op tot de vergrendeling vrijkomt. Dit betekent dat de thread andere coroutines kan uitvoeren terwijl de huidige wacht op 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 — blokkeert thread niet
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Belangrijkste kenmerken van kotlinx Mutex: niet-reentrant — in tegenstelling tot ReentrantLock kan een coroutine niet opnieuw een Mutex verwerven die hij al bezit. Indien nodig, gebruik dan Semaphore(1) in plaats van Mutex. Bovendien is Mutex van kotlinx.coroutines niet-blokkerend: het gebruikt onderbreking via suspend, waardoor de poolthread niet wordt geblokkeerd.
In de praktijk heeft suspending Mutex de voorkeur boven klassieke ReentrantLock in coroutine-code om twee redenen: schaalbaarheid — een coroutine wacht op Mutex, terwijl de thread andere coroutines bedient, wat de doorvoer van het systeem verhoogt; afwezigheid van BlockedThread — er wordt geen bronnen verbruikt voor het opslaan van de stack van de geblokkeerde thread. Volgens JetBrains (Kotlin Coroutines Guide, 2024) verhoogt het gebruik van suspending Mutex de doorvoer met 40% bij 100+ coroutines.
Veelgestelde vragen
Eigendom (ownership) — het fundamentele verschil. Mutex onthoudt welke thread hem heeft verworven en alleen deze thread kan hem vrijgeven. Een binaire semafoor (Semaphore(1)) heeft geen eigenaar — elke thread kan release() uitvoeren. Daarom is Mutex veiliger: een andere thread kan niet per ongeluk iemands anders vergrendeling vrijgeven, terwijl een semafoor dat wel kan.
synchronized is eenvoudiger en korter — gebruik het voor eenvoudige kritieke secties zonder timeouts en zonder controle op eerlijkheid. Gebruik ReentrantLock wanneer TryLock met timeout, fair-planning, Condition Variables of het onderbreken van een wachtende thread (lockInterruptibly) nodig is. Gebruik voor coroutines altijd kotlinx.coroutines.sync.Mutex.
Spinlock — is een vergrendeling waarbij de thread niet slaapt, maar in een lus (spin) de status van de vergrendeling controleert. Spinlock verbruikt CPU, maar schakelt geen context, wat het gunstig maakt voor korte kritieke secties (tot 10 instructies). Mutex brengt de thread in de status BLOCKED, wat 10-50 microseconden duurder is vanwege contextomschakeling, maar geen CPU verbruikt.
Op kernelniveau van Linux is Mutex geïmplementeerd via futex (fast userspace mutex). De thread probeert eerst de vergrendeling in userspace te verwerven via een atomaire CAS-instructie (Compare-And-Swap). Als Mutex vrij is — vindt verwerving plaats zonder syscall. Indien bezet — doet de thread een syscall futex(FUTEX_WAIT) en gaat slapen. Bij vrijgave wekt syscall futex(FUTEX_WAKE) een wachtende thread.
Ja, er bestaan inter-process Mutexen (inter-process mutex). In Windows is dit Named Mutex, in Linux — pthread_mutexattr_setpshared met attribuut PTHREAD_PROCESS_SHARED. In Android ondersteunt Bionic libc ook inter-process Mutexen via bestandsdescriptoren. Inter-process Mutexen worden gebruikt voor synchronisatie tussen verschillende applicaties of tussen een proces en zijn kindprocessen.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook