1. Mutex Lock
A mutex turns a multi-instruction read-modify-write into one that no other thread can interleave with, by forcing every thread to acquire exclusive ownership of a single lock object before touching the shared variable and release it only after the write commits — so the OS may still pause a thread mid-update, but no other thread can observe or overwrite the half-finished state.
The experiment
Two threads each run counter++ 100 times. The correct final value is 200. To make the race visible on every run instead of once in a million, we split counter++ into its three real steps — read into temp, pause, write back temp + 1 — and sleep 1 ms in the middle. That sleep widens the window in which one thread holds a stale temp while the other writes, which is exactly the window a mutex must close.
public class Solution {
private static int counter = 0;
private static final Object lock = new Object();
public static void runExperiment(String name, Runnable task) {
counter = 0;
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
try { t1.join(); t2.join(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Final counter (" + name + "): " + counter);
}
// CORRECT: read-pause-write is one indivisible critical section.
static void withMutex() {
for (int i = 0; i < 100; i++) {
synchronized (lock) { // monitorenter on `lock`
int temp = counter; // read
sleepOneMs(); // pause (still holding the lock)
counter = temp + 1; // write
} // monitorexit on `lock`
}
}
// BUGGY: nothing protects the read-pause-write triple.
static void noMutex() {
for (int i = 0; i < 100; i++) {
int temp = counter; // read (may already be stale)
sleepOneMs(); // pause
counter = temp + 1; // write (clobbers the other thread)
}
}
static void sleepOneMs() {
try { Thread.sleep(1); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public static void main(String[] args) {
runExperiment("with mutex", Solution::withMutex); // always 200
runExperiment("no mutex", Solution::noMutex); // typically ~100..120
}
}Why the naive version is wrong — traced interleaving
Take just the very first iteration of both threads with counter == 0. Without the mutex, the OS scheduler is free to run them in this order (the 1 ms sleep makes this the likely order, not a rare one):
| Step | Thread A | Thread B | counter |
|---|---|---|---|
| 1 | temp = counter → A's temp = 0 | — | 0 |
| 2 | sleep(1) — A pauses | temp = counter → B's temp = 0 | 0 |
| 3 | still sleeping | sleep(1) — B pauses | 0 |
| 4 | counter = temp + 1 → writes 1 | still sleeping | 1 |
| 5 | — | counter = temp + 1 → writes 1 | 1 |
Two increments happened; counter is 1, not 2. B read 0 before A's write landed, so B's write at step 5 silently overwrites A's. This is a lost update. Repeated across 200 increments, the lost ones drag the total down to roughly 100–120 — almost never 200, and a different wrong number each run.
The mutex version cannot reach step 2: B's synchronized (lock) blocks until A's block finishes, so A's read, pause, and write run as an unbroken unit. B then reads counter == 1 and writes 2. Both increments are preserved.
What synchronized (lock) actually compiles to
The keyword is not magic — javac emits two JVM bytecodes around the block: monitorenter on entry and monitorexit on exit (plus a hidden second monitorexit on an exception edge, so the lock is released even if the body throws). Every Java object has an associated monitor: a hidden owner field plus a queue of waiting threads.
monitorenter lock: iflock's monitor is unowned, this thread becomes owner and the recursion count goes to 1. If this thread already owns it, the count just increments (monitors are reentrant). If another thread owns it, this thread blocks on the monitor's entry queue.monitorexit lock: decrements the count; at 0 the monitor is released and one waiting thread is unparked to compete for it.
Crucially, monitorenter/monitorexit also impose a happens-before edge under the Java Memory Model: everything a thread wrote before releasing the lock is visible to the next thread that acquires it. So the mutex fixes two problems at once — mutual exclusion (no interleaving) and visibility (no stale cached counter).
Go: the same logic with sync.Mutex (and the channel alternative)
Go threads are goroutines — lightweight, multiplexed by the Go runtime onto a small pool of OS threads (the M:N scheduler), so spawning thousands is cheap, unlike Java's one-OS-thread-per-Thread model. The lock itself is analogous: sync.Mutex's Lock()/Unlock() play the role of monitorenter/monitorexit, and defer mu.Unlock() is the idiomatic equivalent of the JVM's exception-safe release.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
var wg sync.WaitGroup
counter := 0
increment := func() {
defer wg.Done()
for i := 0; i < 100; i++ {
mu.Lock() // like monitorenter
temp := counter // read
time.Sleep(time.Millisecond) // pause, still holding lock
counter = temp + 1 // write
mu.Unlock() // like monitorexit
}
}
wg.Add(2)
go increment()
go increment()
wg.Wait()
fmt.Println("Final counter:", counter) // always 200
}Go's motto is "share memory by communicating, not communicate by sharing memory." The channel version owns the counter in one goroutine and sends deltas to it — no lock at all, because only one goroutine ever touches the variable:
func main() {
deltas := make(chan int)
done := make(chan int)
go func() { // sole owner of `counter`
counter := 0
for d := range deltas {
counter += d
}
done <- counter
}()
var wg sync.WaitGroup
wg.Add(2)
for w := 0; w < 2; w++ {
go func() {
defer wg.Done()
for i := 0; i < 100; i++ { deltas <- 1 }
}()
}
wg.Wait()
close(deltas)
fmt.Println("Final counter:", <-done) // 200
}Java's nearest analogue to channels is a BlockingQueue plus wait/notify; Go bakes the queue and the blocking handoff into the language as a first-class channel. Pick the mutex when threads genuinely share one piece of state; pick the channel when you can give a single owner exclusive responsibility for it.
Pitfalls
- Locking the wrong object.
synchronized (lock)only excludes threads that synchronize on the same instance. Two threads locking two different objects (e.g. a non-staticlock when the counter isstatic, orsynchronizedon a freshlynew-ed object each call) get no mutual exclusion at all — the bug looks fixed but isn't. - Forgetting to release on the error path. With a raw
java.util.concurrent.locks.Lockyou mustunlock()in afinallyblock; an earlyreturnor thrown exception otherwise leaks the lock forever and deadlocks the next thread.synchronizedand Go'sdefer mu.Unlock()handle this for you — prefer them. - Holding the lock during slow work. Our 1 ms sleep is inside the critical section to demonstrate exclusion, but in real code, doing I/O or sleeping while holding a mutex serializes everything and destroys throughput. Keep critical sections tiny — read, mutate, release.
- Lock ordering → deadlock. The moment you hold two mutexes, threads that acquire them in opposite orders can deadlock. Always acquire multiple locks in a single global order.
- Reaching for a lock when an atomic would do. A pure counter is better served by
AtomicInteger.incrementAndGet()(Java) oratomic.AddInt64(Go), which do the read-modify-write in one lock-free CAS instruction. Use a mutex when the critical section spans multiple variables.
Takeaways
- A mutex makes a multi-step read-modify-write atomic with respect to other threads — the OS can still preempt the holder, but no one else can enter the critical section meanwhile.
synchronized (lock)compiles tomonitorenter/monitorexitonlock's monitor, and also creates a happens-before edge that flushes the holder's writes to the next acquirer — it buys mutual exclusion and visibility.- The lost-update bug appears because two threads read the same stale value before either writes; the lock removes the interleaving that makes the stale read possible.
- Java locks real OS threads; Go multiplexes cheap goroutines and offers channels as a lock-free alternative — same goal (no shared-state race), different mechanism.
Sources: Brian Goetz et al., Java Concurrency in Practice (ch. 2–3, on atomicity, locking, and happens-before); the JVM Specification §6.5 (monitorenter/monitorexit semantics) and JLS §17.4 (Java Memory Model); the Go Memory Model and Effective Go (goroutines, sync.Mutex, channels, "share memory by communicating"). Re-authored and deepened for this guide: added the traced first-iteration interleaving, the monitorenter/monitorexit compilation note, a with-vs-without diagram, a "why the naive version is wrong" explanation, and the Go mutex + channel counterparts.
🤖 Don't fully get this? Learn it with Claude
Stuck on 1. Mutex Lock? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **1. Mutex Lock** (Concurrency) and want to truly understand it. Explain 1. Mutex Lock from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **1. Mutex Lock** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **1. Mutex Lock** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **1. Mutex Lock** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.