The full picture
| Mutex | Semaphore | Spinlock | |
|---|---|---|---|
| Semantics | ownership, exclusive | counter of permits | exclusive, busy-wait |
| Blocked waiter | sleeps (yields CPU) | sleeps | spins (burns CPU) |
| Right when | critical sections | bounding N resources; signaling | sub-µs holds, no preemption (kernel) |
| Java face | synchronized / ReentrantLock | Semaphore; also BlockingQueue permits | spin phase of adaptive locks |
| Cost profile | CAS if uncontended; futex sleep if not | same | zero latency to acquire, CPU waste while waiting |
- The futex insight (worth stating explicitly): 'locks are syscalls' is false on Linux since 2003 — fast path is pure user-space CAS;
futex(FUTEX_WAIT)only on contention. That's why uncontendedsynchronizedis nearly free (JVM biased/thin-lock history is the same idea one layer up). - Adaptive/hybrid locks: spin briefly (maybe the holder finishes in 200ns), then sleep — best of both; this is what JVM monitors and pthread adaptive mutexes actually do. Pure spinning in user space fails catastrophically when the holder gets descheduled: you spin your whole quantum against a sleeping owner.
- Semaphore as signaling: init 0, worker
waits, producersignals — a one-shot event; this no-ownership property is exactly why semaphores can be released by a different thread than acquired (and why that's also their footgun). - Bounded-resource idiom:
Semaphore(50)in front of a downstream is the concurrency-limit pattern that replaced pool-size-as-limiter in the virtual-thread era — an OS primitive solving a 2026 microservice problem.