Knowledge Guide
HomeConcurrencyConcurrency Foundations

Synchronization Constructs

Every synchronization construct on this page is the same machine wearing different clothes: an atomic counter (or flag) guarding shared state, plus a wait-queue of threads parked by the OS — a thread that cannot proceed atomically checks the counter, fails, and is descheduled onto the queue until another thread changes the counter and wakes it. Mutex, read/write lock, semaphore, condition variable, and barrier differ only in what the counter counts and what wakes the waiters. Learn that one mechanism and the five constructs stop being five things to memorize.

This page is the map for the lessons that follow (Mutex, Read/Write Locks, Semaphore, Condition Variables, Barriers). Below, each construct is reduced to its counter + wake rule, then one — the counting semaphore — is traced end to end with real numbers so the machinery is concrete before you meet it again in depth.

ConstructWhat the counter holdsBlock whenWho wakes a waiter
Mutexowner = 1 thread or nonealready owned by anotherthe owner unlocks
Read/Write lockreader count (≥0) + 1 writer flagwant write & readers>0, or want read & writer heldlast reader leaves / writer releases
Semaphoreinteger permits Npermits == 0any thread that releases a permit
Condition variable(no count) a predicate + a wait setpredicate falsea thread that makes the predicate true and signals
Barrierarrived count, target = partiesarrived < partiesthe last party to arrive (trips the barrier)

Note the family resemblance: a mutex is a binary semaphore (permits ∈ {0,1}) with an ownership rule, and a read/write lock is two coupled counters. The condition variable is the odd one out — it owns no resource count; it is pure "park me until someone says the world changed," and is the primitive the others are often built from.

Traced example — a counting semaphore as a connection pool

Concrete setup: a database connection pool with 3 connections, so a semaphore initialized to permits = 3. Five worker threads (W1..W5) each want one connection, hold it briefly, then return it. acquire() decrements permits if >0 else parks the caller; release() increments permits and wakes one parked thread. Here is the exact interleaving with real values — watch the counter and the wait-queue, the two pieces of state that are the semaphore.

StepEventpermitsWait-queue (parked)Holding a connection
0initial3[]
1W1 acquire2[]W1
2W2 acquire1[]W1 W2
3W3 acquire0[]W1 W2 W3
4W4 acquire → permits==0, park0[W4]W1 W2 W3
5W5 acquire → permits==0, park0[W4, W5]W1 W2 W3
6W2 release → wake head0→1→0[W5]W1 W3 W4
7W1 release → wake head0→1→0[]W3 W4 W5
8W3 release1[]W4 W5
9W4, W5 release3[]

The subtle steps are 6 and 7. release() raises permits to 1, but instead of letting permits stay at 1, it immediately hands that permit to the woken waiter (W4, then W5) — so permits is back to 0 and the woken thread proceeds without re-competing. Whether the hand-off is direct (fair) or whether the woken thread must re-race a fresh acquire() (barging, faster but unfair) is the single most important fairness knob, and it is exactly where Java and Go differ — see the code.

diagram
diagram

The core logic in Java and Go

The same connection-pool semaphore, side by side. Java exposes a real Semaphore class backed by AbstractQueuedSynchronizer (the atomic-counter-plus-park engine described above). Go has no semaphore type in the language; the idiomatic equivalent is a buffered channel of capacity 3 — sending a value is acquire, receiving is release, and the channel's internal buffer is the permit counter.

Java — java.util.concurrent.Semaphore

import java.util.concurrent.Semaphore;

Semaphore pool = new Semaphore(3, /* fair = */ true);

void useConnection() throws InterruptedException {
    pool.acquire();          // blocks & parks if permits == 0
    try {
        // ... use one of the 3 connections ...
    } finally {
        pool.release();      // ALWAYS release, even on exception
    }
}

Go — buffered channel as a counting semaphore

// capacity 3 == 3 permits; the buffer is the counter
sem := make(chan struct{}, 3)

func useConnection() {
    sem <- struct{}{}        // acquire: blocks if buffer is full
    defer func() { <-sem }() // release: frees one slot
    // ... use one of the 3 connections ...
}

Where the runtimes differ. A blocked Java thread parks an OS thread (a 1:1 mapping; ~1 MB stack, a kernel context-switch to wake). A blocked goroutine parks only the goroutine — the Go scheduler (M:N) hands the underlying OS thread to another runnable goroutine, so blocking is cheap (~KB stacks, no kernel switch) and you can have hundreds of thousands of waiters. Java's wake comes from LockSupport.unpark; Go's comes from the channel send making a receiver runnable. And Java Semaphore lets you choose fair (FIFO hand-off) vs. unfair (barging); a Go channel's send/receive ordering is FIFO among waiters but you do not get a non-permit-based fairness toggle.

Why the naive "check-then-act" version is wrong

A tempting hand-rolled semaphore looks like if (permits > 0) permits--;. This is a race: two threads can both read permits == 1, both pass the check, and both decrement to -1 — handing out a fourth connection that does not exist. The decrement must be atomic with the check (a CAS loop or a lock-protected counter), which is exactly why you reach for the library primitive instead of an int and an if. Likewise, parking must be done under the same lock that guards the counter, or a thread can decide to park after the only releaser has already signalled — and sleep forever (a lost wakeup). The library handles both; your if does not.

Pitfalls

Takeaways


Re-authored and deepened for this guide. Sources: Doug Lea, The java.util.concurrent Synchronizer Framework (the AQS paper underlying Java's Semaphore, ReentrantLock, and CyclicBarrier); the OpenJDK java.util.concurrent API docs (Semaphore, ReentrantReadWriteLock, Condition, CyclicBarrier); the Go Programming Language Specification and the Go Blog, "Share Memory By Communicating" (channels as the idiomatic concurrency primitive); and Dijkstra's original formulation of the semaphore (the P/V operations). The atomic-counter-plus-wait-queue framing follows Herlihy & Shavit, The Art of Multiprocessor Programming.

🤖 Don't fully get this? Learn it with Claude

Stuck on Synchronization Constructs? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Synchronization Constructs** (Concurrency) and want to truly understand it. Explain Synchronization Constructs 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.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Synchronization Constructs** 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.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Synchronization Constructs** 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.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Synchronization Constructs** 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.

📝 My notes