Coroutines can run on different threads. Even on one thread, suspending between a check and an update can let another coroutine change the same state. A variable declaration does not make a sequence of operations atomic. Shared data needs a clear ownership or synchronization rule.
Review the coroutines lesson first. These examples use Kotlin/JVM and kotlinx-coroutines-core 1.10.2 from Maven Central. They demonstrate both a protected counter and a stock reservation invariant. The useful question is not whether one run happens to print the right number, but whether every permitted interleaving preserves the rule.
Read, compute and write is a compound operation
Counter++ looks like one expression, but conceptually it reads a value, computes a successor and writes it. Two workers can read the same old value and both write the same successor, losing an increment. A race may appear only occasionally and can change when logging or debugging alters scheduling.
Volatile improves visibility and the guarantees around individual reads and writes on the JVM. It does not combine a read-modify-write operation into one indivisible update. A volatile counter can therefore still lose increments. Use an atomic operation when the entire required action is supported by an atomic type.
For a simple JVM counter, AtomicInteger.incrementAndGet is one option. For an invariant involving several fields or a check followed by mutation, a Mutex-protected critical section often expresses the rule more naturally. Do not try to compose independently atomic field operations and assume their combined business transaction is automatically atomic.
Lock the entire invariant and every access path
A mutex provides mutual exclusion only to code that cooperates by acquiring that same mutex. Protecting writes while allowing unrelated unsynchronized reads can still expose races or inconsistent state. Hide the fields behind operations that enforce the chosen rule, rather than rely on every caller to remember a comment.
WithLock acquires the mutex and releases it through a finally-style pattern if the action fails. Coroutine mutex acquisition suspends the waiting coroutine rather than occupying a thread solely while waiting. The protected action should still be small; do not hold the lock across slow network calls or unrelated work.
Mutex is not reentrant. A function already holding it should not call another function that tries to acquire the same mutex and expect automatic recursive acquisition. Structure internal helpers so they clearly require the lock or operate on already validated local values. Consistent lock ordering also matters if an operation truly needs more than one lock.
Ownership alternatives and external consistency
Confinement keeps state access within one chosen execution owner. It can be efficient when whole operations stay together, but merely launching some writes on one dispatcher is insufficient if other code still touches the fields elsewhere. The ownership rule must cover all reads and updates, and suspensions can still require attention to multi-step invariants.
Immutable snapshots reduce shared mutation. A coroutine can compute a new value privately and publish it through a suitable atomic or state-management mechanism. Copying an object that contains mutable children is not automatically an independent snapshot, so inspect the whole data structure.
An in-process mutex does not coordinate separate application processes or protect a database from other clients. A persistent stock reservation needs database transactions, conditional updates or another server-side consistency mechanism. Use this lesson's mutex for local memory invariants, and choose a different boundary when the shared resource lives outside this process.
Official references and practical review
The shared-state guide compares atomics, confinement and mutual exclusion. Read the Mutex contract and withLock API before designing nested operations. A review should name the invariant, its owner, every access path and the external resources beyond the local lock's reach.
Kotlin Shared Mutable State and Mutex Examples
Each example shows the code, the output it produces and what each part does.
Count every protected increment
Each increment holds the same mutex. CoroutineScope waits for all ten children, so the final read occurs after their updates finish. The code does not promise a particular worker order; the invariant is that exactly one thousand increments are included regardless of order.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
fun main() = runBlocking {
val mutex = Mutex()
var counter = 0
coroutineScope {
repeat(10) {
launch(Dispatchers.Default) {
repeat(100) { mutex.withLock { counter++ } }
}
}
}
println(counter)
}1000Kotlin Shared Mutable State and Mutex in Real Projects
Worked exercise: Reserve local stock without going negative
Start with seven units and let ten coroutines each request one. Check availability and decrement inside the same protected section. Return a Boolean so callers know whether their reservation succeeded. A protected query exposes the remaining count safely.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class LocalStock(initial: Int) {
init { require(initial >= 0) }
private val mutex = Mutex()
private var units = initial
suspend fun reserveOne(): Boolean = mutex.withLock {
if (units == 0) false else { units--; true }
}
suspend fun remaining(): Int = mutex.withLock { units }
}
fun main() = runBlocking {
val stock = LocalStock(7)
val accepted = coroutineScope {
List(10) { async(Dispatchers.Default) { stock.reserveOne() } }.awaitAll()
}
println(accepted.count { it })
println(stock.remaining())
}7
0Which callers win is intentionally unspecified. The result count and nonnegative remainder are stable. Moving the availability check outside the lock would split the invariant and allow several callers to act on the same stale count. Test zero initial stock and more requests than available units, not just a quiet single-caller path.
Do not write a test that insists a particular caller must win unless the API explicitly promises that ordering. A correct mutual-exclusion design protects state, while scheduling and business priority are separate policies. If reservations need a priority queue, model that queue directly instead of expecting the mutex to infer it.
Kotlin Shared Mutable State and Mutex FAQ
Does one dispatcher mean races are impossible?
Not necessarily. Different threads can execute on multithreaded dispatchers, and even single-threaded coroutine code can interleave at suspension points. A read, suspension and later write can use stale information after another coroutine changes the value. A confinement strategy must define one owner and preserve the required operation boundaries. Do not infer correctness merely from a dispatcher name or from the fact that one test run printed the expected count.
Why is volatile insufficient for counter++?
Volatile concerns the visibility and atomicity of individual variable accesses on the JVM. Increment combines a read, arithmetic and a write, so two workers can still overlap those steps and lose an update. Use an atomic increment operation for a simple counter or protect the compound action with a shared mutex. When several fields participate in one invariant, consider the whole state transition rather than marking each field independently volatile.
Can a mutex protect data that another function reads freely?
Only cooperating access receives the synchronization guarantees you designed. A function that bypasses the mutex can observe changing or inconsistent state while another coroutine updates it. Encapsulate fields and offer protected queries and updates so callers cannot accidentally bypass the rule. After all writers have completed and ownership has clearly transferred, an unprotected read may be appropriate, but document that lifetime boundary instead of assuming it exists everywhere.
Should a stock API keep a lock while calling a server?
Usually avoid holding a local lock across slow remote work. It can serialize unrelated callers for a long time and complicate cancellation or reentrancy. More importantly, a local mutex cannot enforce consistency across other clients of that server. Use the server's transactional or conditional-update contract for authoritative stock. Local state can track pending work under a short lock, then reconcile results through an explicit policy when the external operation finishes.
Kotlin Shared Mutable State and Mutex Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which statement accurately describes an in-process Mutex?
Kotlin Shared Mutable State and Mutex Quiz
2 questions to check what you learned.
Why can two concurrent counter++ operations lose an update?
- Every coroutine gets a private automatic copy of the variable.
- Int cannot represent positive values.
- Both can read the same old value before either writes its successor.
What must the stock reservation lock cover?
- The availability check and decrement as one protected operation.
- Only the final print statement.
- Only the variable declaration.