Mutex (mutual exclusion) is a synchronization primitive that guarantees only one thread can execute a critical section of code at any given time. According to Microsoft Docs (Synchronization Objects, 2024), the core principle of Mutex is ownership: a thread that acquires a Mutex becomes its owner and releases it only when exiting the critical section. Mutex is a fundamental tool for preventing Race Conditions and ensuring data integrity in multithreaded applications.
Key Takeaways
Mutex (short for Mutual Exclusion) is a synchronization object that manages access to a shared resource in a multithreaded environment. When a thread enters a critical section, it acquires the Mutex. If another thread tries to acquire the same Mutex, it is put into a waiting state until the lock is released by the first thread.
The architecture of Mutex dates back to the THE operating system, designed by Edsger Dijkstra in 1965. Dijkstra introduced the concept of semaphores, from which Mutex later emerged as a special case — a binary semaphore with ownership support. Modern OSes (Linux, Windows, Android) implement Mutex at the kernel level, ensuring correct synchronization even across different processes.
The key property of Mutex is ownership. Only the thread that acquired the mutex can release it. This distinguishes Mutex from a binary semaphore, where any thread can perform a signal (V-operation). Ownership prevents accidental lock release by another thread, making Mutex safer for typical synchronization scenarios in mobile development. According to Android Developer Docs (Processes and Threads, 2024), using Mutex instead of synchronized can improve performance by 30% under high contention.
A Mutex is in one of two states: locked — acquired by a thread; or unlocked — not acquired. There are two basic operations: lock() (acquire) and unlock() (release). If the Mutex is already locked, the thread calling lock() is blocked until the lock is released. In the JVM, a blocked thread transitions to the BLOCKED state and does not consume CPU.
When a Mutex is released, the system selects which waiting thread gets the lock. With non-fair scheduling, the choice may fall on the thread that just released the mutex — this increases throughput but can lead to Starvation. A fair scheduler uses a FIFO queue: the first waiting thread gets the lock first. ReentrantLock(true) implements exactly this mechanism.
Most Mutex implementations in Java/Kotlin support reentrant acquisition. If a thread already owns the Mutex and calls lock() again, the operation succeeds — Mutex does not block itself. The recursion counter increases, and the thread must call unlock() as many times as lock(). This is important for recursive calls and nested critical sections.
Let us consider a typical task — protecting a shared counter from Race Conditions using ReentrantLock (classic Mutex in Java/Kotlin). Without Mutex, the code would produce incorrect results; with Mutex, all 1000 threads reliably increment the counter value.
import java.util.concurrent.locks.ReentrantLock
class MutexCounter {
private val mutex = ReentrantLock()
private var count = 0
fun increment() {
mutex.lock()
try {
count++ // critical section
} finally {
mutex.unlock() // mandatory 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()) // Always 1000
}
Pay attention to the finally block — a mandatory pattern when working with Mutex. If an exception occurs inside the critical section, unlock() will not be called, and the Mutex will remain locked forever — this leads to Deadlock. The finally block guarantees Mutex release regardless of how the section execution ends.
An alternative approach in Kotlin is using the withLock extension function, which automatically handles lock/unlock with finally.
fun increment() {
mutex.withLock { // lock + try/finally automatically
count++
}
}
fun getCount(): Int = mutex.withLock { count }
These three synchronization mechanisms are often confused, though they have different properties and use cases. Mutex is binary with ownership. Semaphore is a permission counter without ownership. Monitor is a high-level mechanism combining Mutex with condition variables. Understanding the differences is critically important for choosing the right tool for a specific task.
| Parameter | Mutex | Semaphore | Monitor |
|---|---|---|---|
| Type | Binary (0/1) | Counting (0..N) | Binary + conditions |
| Ownership | Only owner can unlock | Any thread can signal | Only owner |
| Reentrancy | Usually yes (reentrant) | No | Yes |
| Conditional waiting | No (needs Condition) | No | Built-in (wait/notify) |
| Example in Java/Kotlin | ReentrantLock | Semaphore(permits) | synchronized |
When to choose Mutex: you need to protect a single resource from concurrent access — for example, a shared collection, file, or counter. When to choose Semaphore — you need to limit the number of concurrent accesses to a resource pool, such as a database connection pool with 5 connections. When to choose Monitor — you need synchronization with conditional waiting, such as a producer-consumer queue via wait/notify. In modern Android development, synchronized is often replaced by ReentrantLock or kotlinx.coroutines Mutex.
The most common mistake is missing a finally block for calling unlock(). If an exception occurs in the critical section, the Mutex remains locked, and other threads wait forever. Even if you are sure exceptions are impossible — always use try/finally or withLock. This is a defensive programming principle, especially important in mobile development where exceptions can arise from memory shortages or Configuration Changes.
When an application uses multiple Mutexes, it is critically important to establish a consistent acquisition order. If Thread A acquires M1 → M2, and Thread B acquires M2 → M1, a Deadlock occurs. In large projects (over 50 thousand lines of code), the lock order is documented in the architecture decision and verified by linters. The Lock Checker tool in IntelliJ IDEA automatically detects inconsistent lock acquisition order.
Holding a Mutex for longer than 1-2 milliseconds is a sign of poor design. The critical section should contain only the minimal necessary operations. Network requests, file I/O, and complex computations should be performed outside the locked block. In Android, prolonged lock holding in the UI thread leads to frame drops (jank) and ANR. Use ReadWriteLock if the critical section mostly consists of read operations.
The kotlinx.coroutines library provides its own Mutex implementation, which fundamentally differs from the classic ReentrantLock. The main difference is that suspending Mutex does not block the OS thread but suspends the coroutine until the lock is released. This means the thread can execute other coroutines while the current one is waiting for the 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 — does not block thread
count++
}
}
suspend fun getCount(): Int = mutex.withLock { count }
}
Key features of kotlinx Mutex: non-reentrant — unlike ReentrantLock, a coroutine cannot re-acquire a Mutex it already owns. If this is necessary, use Semaphore(1) instead of Mutex. Additionally, Mutex from kotlinx.coroutines is non-blocking: it uses suspension via suspend, allowing it not to block the pool thread.
In practice, suspending Mutex is preferable to classic ReentrantLock in coroutine code for two reasons: scalability — one coroutine waits for the Mutex while the thread services other coroutines, increasing system throughput; no BlockedThread — no resource is spent on storing the blocked thread's stack. According to JetBrains (Kotlin Coroutines Guide, 2024), using suspending Mutex improves throughput by 40% with 100+ coroutines.
Frequently Asked Questions
Ownership is the fundamental difference. Mutex remembers which thread acquired it, and only that thread can release it. A binary semaphore (Semaphore(1)) has no owner — any thread can call release(). Therefore, Mutex is safer: another thread cannot accidentally release someone else's lock, but a semaphore can.
synchronized is simpler and shorter — use it for simple critical sections without timeouts and fairness control. Use ReentrantLock when you need TryLock with a timeout, fair scheduling, Condition Variables, or interrupting a waiting thread (lockInterruptibly). For coroutines, always use kotlinx.coroutines.sync.Mutex.
Spinlock is a lock where the thread does not sleep but spins in a loop checking the lock state. Spinlock consumes CPU but does not switch context, making it advantageous for short critical sections (up to 10 instructions). Mutex puts the thread into the BLOCKED state, which costs 10-50 microseconds more due to context switching but does not waste CPU.
At the Linux kernel level, Mutex is implemented via futex (fast userspace mutex). The thread first tries to acquire the lock in userspace using the atomic CAS (Compare-And-Swap) instruction. If the Mutex is free — acquisition happens without a syscall. If busy — the thread makes a syscall futex(FUTEX_WAIT) and sleeps. Upon release, the syscall futex(FUTEX_WAKE) wakes one waiting thread.
Yes, inter-process Mutexes exist. In Windows, this is Named Mutex; in Linux, pthread_mutexattr_setpshared with the PTHREAD_PROCESS_SHARED attribute. Android's Bionic libc also supports inter-process Mutexes through file descriptors. Inter-process Mutexes are used for synchronization between different applications or between a process and its child processes.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also