Synchronization Primitives

Mutexes, reentrant locks, semaphores, condition variables, read-write locks, barriers and events: what each one answers, how it behaves in Python and Java, and a running test for each.

Overview

A synchronization primitive is a small object that threads use to coordinate: to take turns on shared data, to limit how many run at once, or to wait until something has happened. Every higher-level tool, from a blocking queue to a thread pool, is built from a few of them.

Interviewers test whether you pick the right one and use it correctly. Most bugs come from using a good primitive the wrong way: waiting on a condition with if instead of while, releasing a semaphore twice, or taking a lock that is already held by the same thread.

Pick by the question you are asking

  • Only one thread at a time in this code? — Mutex (lock) — threading.Lock — synchronized, ReentrantLock
  • May the same thread take it again? — Reentrant lock — threading.RLock — ReentrantLock, synchronized
  • At most N threads at a time? — Counting semaphore — Semaphore(n), BoundedSemaphore(n) — Semaphore
  • Wait until some state is true? — Condition variable — threading.Condition — Condition, Object.wait
  • Many readers or one writer? — Read-write lock — none in the standard library — ReentrantReadWriteLock, StampedLock
  • Wait until all N threads arrive? — Barrier — threading.Barrier — CyclicBarrier
  • Wait for a one-time signal? — Event or latch — threading.Event — CountDownLatch (one-shot)

Mutex and reentrant lock

A mutex lets one thread at a time into a critical section. A plain threading.Lock is not reentrant: if the thread that holds it asks again, it blocks, on itself. A reentrant lock (RLock) counts how many times its owner has taken it and is free only after the same number of releases. Java's synchronized and ReentrantLock are both reentrant.

The two also differ in ownership: the Python docs say a Lock may be released from any thread, while an RLock must be released by the thread that acquired it.

Semaphore

A semaphore holds a count of permits. acquire takes one, waiting if none are left; release returns one. It limits how many threads do something at once: connections to a database, calls to a rate-limited API. A semaphore has no owner, so any thread may release it; Java's docs note this is useful in special cases such as deadlock recovery. A BoundedSemaphore in Python raises ValueError if it is released more times than acquired, which catches a common bug.

Condition variable

A condition variable lets a thread sleep until another thread changes some state and tells it. It is always paired with a lock: the waiter checks the state under the lock, and wait releases the lock while sleeping and takes it back before returning. Both Python's and Java's docs say to wait in a loop, because a wakeup does not prove the state is what you need: another thread may have changed it first, and Java allows spurious wakeups.

Read-write lock

A read-write lock lets many readers in together, or one writer alone. It pays off only when reads are frequent and slow enough that letting them overlap saves more than the lock's extra bookkeeping. The Java docs put it this way: worthwhile when the data is large, readers outnumber writers, and operations cost more than the synchronization.

Python's threading has no read-write lock, so here is one built from a condition variable. It prefers writers: once a writer is waiting, new readers wait, so a steady stream of readers cannot starve it.

Barrier and event

A barrier holds a fixed number of threads until all of them arrive, then releases them together; it suits phased work (every worker finishes step 1 before any starts step 2). An event is a flag that threads wait on until someone sets it; it suits a one-time signal such as 'config loaded' or 'shut down'. Java's CountDownLatch is one-shot and cannot be reset; its docs point to CyclicBarrier when you need to reuse it.

What you will be able to do

  • Pick the primitive from the question: one at a time, at most N, wait for state, many readers, all arrive, one signal.
  • Explain ownership and reentrancy, and why a binary semaphore is not a mutex.
  • Write a condition wait correctly, in a loop, and say why.
  • Build a writer-preferring read-write lock and say when it is worth it.
  • Tell a barrier from a latch and an event.

Interview questions

  • What is the difference between a mutex and a semaphore?
  • When should you use a condition variable instead of polling?
  • What is a read-write lock, and when would you use one?
  • Implement a counting semaphore using only a mutex and a condition variable.
  • What is a reentrant lock, and when does reentrancy matter?

Sign up free to read the full topic

More Low-Level Design topics

Start free on Cariara · Coding problems · System design studio