0Pricing
Kotlin Academy · Lesson

Mutex and Semaphore for Shared State

Protect shared mutable state using Mutex and limit concurrency with Semaphore.

Mutex and Semaphore for Shared State is a free Kotlin Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The Shared State Problem

Coroutines on multiple threads can corrupt shared mutable state. Without synchronization, concurrent increments produce wrong results.

import kotlinx.coroutines.*
fun main() = runBlocking {
    var counter = 0
    val jobs = List(1000) {
        launch(Dispatchers.Default) { counter++ } // race condition!
    }
    jobs.forEach { it.join() }
    println("Expected 1000, got: $counter") // likely wrong
}

Mutex Basics

Mutex is a mutual exclusion lock for coroutines. It suspends (not blocks) the caller until the lock is available.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
    val mutex = Mutex()
    var counter = 0
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            mutex.withLock { counter++ }  // safe
        }
    }
    jobs.forEach { it.join() }
    println("Counter: $counter") // 1000
}

withLock Extension

mutex.withLock { ... } acquires the lock, runs the block, and releases it — even on exception or cancellation.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
    val mutex = Mutex()
    var list = mutableListOf<Int>()
    List(10) {
        launch {
            mutex.withLock {
                list.add(it)
            }
        }
    }.forEach { it.join() }
    println(list.sorted())
}

lock / unlock Manually

You can call lock() and unlock() directly for finer control, but prefer withLock to avoid forgetting to unlock.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
    val mutex = Mutex()
    mutex.lock()
    try {
        println("Critical section")
    } finally {
        mutex.unlock()  // must always release
    }
}

Mutex is Not Reentrant

Kotlin's Mutex is NOT reentrant. Calling lock() again from the same coroutine will deadlock. Use a counter or refactor to avoid nested locking.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
    val mutex = Mutex()
    // This deadlocks:
    // mutex.withLock { mutex.withLock { println("never") } }
    mutex.withLock {
        println("Acquired once — safe")
    }
}

Semaphore Basics

A Semaphore(n) allows up to n coroutines to proceed concurrently. It is like a ticket system with n slots.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
    val semaphore = Semaphore(3) // max 3 concurrent
    List(10) { i ->
        launch {
            semaphore.withPermit {
                println("Concurrent task $i"); delay(100)
            }
        }
    }.forEach { it.join() }
}

withPermit Extension

semaphore.withPermit { ... } acquires a permit, runs the block, and releases — the coroutine-safe equivalent of a semaphore try-acquire.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
fun main() = runBlocking {
    val sem = Semaphore(2)
    val results = mutableListOf<String>()
    List(5) { i ->
        launch {
            sem.withPermit {
                delay(50)
                synchronized(results) { results.add("task-$i") }
            }
        }
    }.forEach { it.join() }
    println(results)
}

Rate Limiting with Semaphore

Use a semaphore to rate-limit concurrent API calls — limit to N requests at a time.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
val apiSemaphore = Semaphore(5) // max 5 concurrent requests
suspend fun callApi(id: Int): String {
    return apiSemaphore.withPermit {
        delay(100) // simulate network
        "Response-$id"
    }
}
fun main() = runBlocking {
    val results = (1..20).map { async { callApi(it) } }.awaitAll()
    println("Got ${results.size} responses")
}

Mutex vs Semaphore

Mutex: binary semaphore (1 permit), for exclusive access. Semaphore(n): n permits, for limited concurrency. Use Mutex to protect a resource; Semaphore to limit parallelism.

import kotlinx.coroutines.sync.*
// Mutex = Semaphore(1) for exclusive access
// Semaphore(n) = n-way concurrency limit
val exclusive = Mutex()          // one at a time
val limited   = Semaphore(3)     // three at a time
fun main() { println("Mutex for exclusion; Semaphore for rate limiting") }

Atomic Counter with Mutex

Implement a thread-safe counter class using Mutex to protect increments and reads.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
class AtomicCounter {
    private val mutex = Mutex()
    private var count = 0
    suspend fun increment() = mutex.withLock { count++ }
    suspend fun get() = mutex.withLock { count }
}
fun main() = runBlocking {
    val counter = AtomicCounter()
    List(100) { launch { counter.increment() } }.forEach { it.join() }
    println("Count: ${counter.get()}")
}

Alternatives: Single-Threaded Dispatcher

Instead of a mutex, confine mutable state to a single-threaded dispatcher. Only one coroutine runs on that dispatcher at a time, making access inherently safe.

import kotlinx.coroutines.*
fun main() = runBlocking {
    val singleThread = newSingleThreadContext("CounterThread")
    var counter = 0
    List(1000) {
        launch(singleThread) { counter++ }
    }.forEach { it.join() }
    println("Counter: $counter") // 1000, no mutex needed
    singleThread.close()
}

Quick Check

What is the key difference between Mutex and Semaphore?

Recap

Mutex provides exclusive access to shared state — prefer withLock. Semaphore(n) limits concurrency to n. Both suspend (not block) waiting coroutines. For simple cases, a single-threaded dispatcher is often cleaner.

Frequently asked questions

Is the “Mutex and Semaphore for Shared State” lesson free?

Yes — the full text of “Mutex and Semaphore for Shared State” is free to read here on the web, and the Kotlin Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Kotlin Academy course, upgrade to CoddyKit PRO.

What will I learn in “Mutex and Semaphore for Shared State”?

Protect shared mutable state using Mutex and limit concurrency with Semaphore. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Kotlin Academy?

No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Mutex and Semaphore for Shared State” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Kotlin Academy lesson?

Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Channel Basics: send, receive, and close
  2. Channel Types: Rendezvous, Buffered, Conflated, Unlimited
  3. Mutex and Semaphore for Shared State
  4. Actors and Structured State Management
← Back to Kotlin Academy