2. Locks, Mutexes & Synchronized Access

The idiomatic mutual-exclusion API in each language -- basic locks, reentrancy, reader/writer variants, and what happens when a language has no shared-memory threading model to lock at all.

Locks are where language ergonomics diverge the most sharply of any topic in this manual: Java and Kotlin inherit a rich java.util.concurrent.locks toolkit, Go has exactly one deliberately minimal sync.Mutex, Python's Lock exists mostly to protect compound operations the GIL doesn't cover, and JavaScript has no lock primitive at all for the common case because there's usually only one thread to contend with.

See roadmap: Locks & Mutual Exclusion

Language Verdict: Pros, Cons & Recommendation

Go

5/5
  • Runtime detects the fully-deadlocked case automatically and crashes with a full stack dump
  • sync.RWMutex built into the standard library, simple two-method API
  • defer mu.Unlock() makes the release-on-panic idiom effectively foolproof
  • sync.Mutex is NOT reentrant -- and Go deliberately provides no reentrant alternative at all
  • TryLock has no timeout parameter; bounded waits must be hand-built

Java

5/5
  • Richest lock toolkit of the five: synchronized, ReentrantLock, ReadWriteLock, StampedLock
  • Reentrant by default -- the safer failure mode for nested locking
  • tryLock with timeout built in
  • No proactive deadlock detection -- you find out via a thread dump after the fact
  • unlock() in a finally block is manual, easy to forget compared to synchronized

Kotlin

4/5
  • Mutex is coroutine-native -- suspends instead of blocking a carrier thread
  • Full interop with Java's lock toolkit when you need reader/writer or StampedLock
  • Mutex is NOT reentrant, a real gotcha for people used to synchronized/ReentrantLock
  • No coroutine-native reader/writer lock -- must fall back to blocking Java locks

Python

3/5
  • acquire(timeout=...) uniformly covers immediate/bounded/blocking modes
  • Explicit Lock vs. RLock choice makes reentrancy an intentional decision
  • No reader/writer lock in the standard library at all
  • GIL means locks mostly protect compound operations, not raw throughput -- easy to reach for one you don't actually need

JavaScript

2/5
  • No lock needed at all for the overwhelmingly common single-threaded case -- one less thing to get wrong
  • Atomics.wait/notify exist for the rare SharedArrayBuffer/worker case
  • Zero built-in coordination primitives once you do have real threads (workers) -- everything is hand-rolled on Atomics
  • No deadlock detection or diagnosis tooling comparable to jstack/pprof/py-spy for the async-hang equivalent
Recommendation: Default to the language's built-in lock (synchronized/ReentrantLock, Mutex, sync.Mutex, Lock/RLock) and always release it in a finally/defer/with/try-finally-equivalent; only Go gives you a safety net (deadlock crash-and-dump) if you get lock ordering wrong, so discipline about consistent lock-acquisition order matters most in Java, Kotlin, and Python.

Concurrency Mechanics, Side by Side

Basic Mutex / Lock API

Must-know
var mu sync.Mutex func withdraw(amount int) { mu.Lock() defer mu.Unlock() // idiomatic -- runs even on panic balance -= amount }

sync.Mutex is Go's one and only basic lock type, with exactly two methods worth knowing day-to-day: Lock() and Unlock(). The idiom is always defer mu.Unlock() immediately after Lock() so the unlock runs even if the function panics -- functionally identical in intent to Java's try/finally, just expressed with Go's defer keyword instead.

Reentrancy: Can a Thread Re-Acquire Its Own Lock?

Must-know

This is a classic interview trap because the 'wrong' answer for a given language is a guaranteed deadlock, not a warning.

var mu sync.Mutex func outer() { mu.Lock() defer mu.Unlock() inner() // DEADLOCKS -- sync.Mutex is NOT reentrant } func inner() { mu.Lock() // blocks forever, mu is already held by this same goroutine defer mu.Unlock() }

sync.Mutex has no concept of 'owning goroutine' at all -- it's a pure counting-free binary lock, so a second Lock() call from the same goroutine blocks exactly as if a different goroutine held it, which means forever. The Go team's stance is explicit: reentrant locks tend to hide design problems (a function that needs to know it already holds a lock is arguably poorly factored), so Go does not provide one in the standard library.

Reader-Writer Locks

Recommended
var rw sync.RWMutex rw.RLock() // many readers can hold this simultaneously defer rw.RUnlock() value := cache[key] rw.Lock() // exclusive defer rw.Unlock() cache[key] = value

sync.RWMutex is built into the standard library with the same RLock/Lock split as Java's ReadWriteLock, and is idiomatic Go for read-heavy shared maps/caches guarded by a mutex (as opposed to sync.Map, which is a different, lock-free-ish concurrent map for a narrower set of access patterns).

try-lock / Timed Acquisition

Recommended
// TryLock (Go 1.18+) -- immediate only, no timeout parameter: if mu.TryLock() { defer mu.Unlock() } else { // busy right now } // A bounded wait is built by hand with a timer + retry or a channel + select: select { case <-acquired: case <-time.After(500 * time.Millisecond): // timed out }

TryLock() was only added in Go 1.18 and is immediate/non-blocking only -- there is no built-in timed variant. A genuinely bounded lock acquisition is usually expressed with a channel-based semaphore pattern and select with time.After instead of trying to force a timeout onto sync.Mutex directly, which is the more idiomatic Go shape for this need anyway.

Deadlock-Prone Patterns & Built-In Detection

Recommended
func main() { var mu sync.Mutex mu.Lock() mu.Lock() // no other goroutine can ever unlock this } // Output: fatal error: all goroutines are asleep - deadlock! // (with a full goroutine dump of every stack, printed automatically)

This is a genuine Go differentiator: the runtime detects the specific case where every goroutine in the program is blocked and none can ever make progress, and crashes immediately with 'fatal error: all goroutines are asleep - deadlock!' plus a dump of every goroutine's stack -- turning a silent hang into an immediate, diagnosable crash. It does not detect deadlocks that involve, e.g., a goroutine blocked on network I/O that will eventually time out; only the true all-blocked case.

Further Reading