Synchronisation (locks, semaphores)
In one line
Synchronisation tools like mutexes, semaphores and condition variables stop threads that share data from interleaving in ways that corrupt it.
What it is
When two threads read and write shared data at the same time, the result can depend on exact timing. This is a race condition. The piece of code that touches the shared data is the critical section, and it must not be run by two threads at once.
A mutex (lock) lets exactly one thread into a critical section at a time. A semaphore is a counter: wait (P) decrements it and blocks at zero, signal (V) increments it and wakes a waiter, so a semaphore of 3 lets up to 3 threads in. A condition variable lets a thread sleep until some condition becomes true, always used together with a mutex. Spinlocks busy-wait instead of sleeping, and atomic operations (like compare-and-swap) update a single value safely without a lock.
Why interviewers ask
Race conditions are some of the hardest bugs in production: they appear rarely, under load, and disappear when you add logging. Interviewers ask about them to check you understand why counter++ is not safe, when to use a mutex vs a semaphore, and how the same ideas appear in databases, caches and distributed systems (row locks, optimistic locking, distributed locks).
How it works
- 1Identify shared mutable state: any variable, object or file that more than one thread can change.
- 2Find the critical section: the smallest block of code that reads and then writes that state and must happen as one step.
- 3Protect it: acquire a mutex before, release after (ideally with a 'with' block or try/finally so it is released even on errors). Only the owner releases a mutex.
- 4Use a counting semaphore when you need to limit concurrency to N (for example N database connections), not exclusive access.
- 5Use a condition variable when a thread must wait for a state change (queue not empty): lock, check the condition in a while loop, wait (which releases the lock while sleeping), and re-check after waking.
- 6Prefer higher-level tools when possible: thread-safe queues, atomic counters, or immutable data that never needs locking.
Worked example · Two threads both run counter += 1 when counter is 5
- counter += 1 is really three steps: read counter into a register, add 1, write it back.
- Thread A reads 5. Before it writes, the scheduler switches to Thread B.
- Thread B reads 5 (A has not written yet), adds 1 and writes 6.
- Thread A resumes with its stale 5, adds 1 and writes 6.
- Two increments happened, but counter is 6, not 7. One update was lost.
- With a mutex: A acquires the lock, reads 5, writes 6, releases. B was blocked trying to acquire; now it reads 6 and writes 7. The result is always correct, at the cost of B waiting briefly.
Picture it
A mutex is the single key to a shop's one-person changing room: you take the key, others wait. A semaphore is a box of 3 keys for 3 changing rooms: up to 3 people at once, the fourth waits for a key to come back.
Reference code
import threading
counter = 0
lock = threading.Lock()
def add(n):
global counter
for _ in range(n):
with lock: # only one thread in the critical section
counter += 1 # read-modify-write is not atomic
threads = [threading.Thread(target=add, args=(100_000,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # always 400000
# A counting semaphore limits how many threads enter, instead of one.
db_slots = threading.Semaphore(3)
def run_query(sql):
with db_slots: # at most 3 queries at the same time
... # use one of 3 database connections
The lock makes the read-modify-write of counter a single step from other threads' point of view, so four threads adding 100,000 each always give 400,000. Without the lock, updates can be lost. The semaphore allows up to three concurrent queries instead of one.
Key patterns
Mutex around shared state
When: Several threads update the same data
How: Keep the critical section as small as possible and always release in a finally or with block.
Semaphore to limit concurrency
When: A resource allows N users at once (connections, API rate)
How: Initialise to N; acquire before use, release after.
Condition variable wait loop
When: A thread must wait until a condition holds
How: Lock, while not condition: wait(); act; unlock. The while loop handles spurious wakeups and races.
Avoid sharing
When: Designing new concurrent code
How: Use message passing (queues, channels), immutable data or atomic operations so fewer locks are needed.
Real-world use
Databases
PostgreSQL and MySQL take row-level locks on UPDATE, and SELECT ... FOR UPDATE lets an application lock rows explicitly, the same idea as a mutex around a critical section, used for things like decrementing stock.
Connection pools
HikariCP in Java and SQLAlchemy's pool limit how many connections are in use at once and make extra requests wait, effectively a counting semaphore around a fixed set of resources.
Java and Go standard libraries
Java offers synchronized, ReentrantLock, Semaphore and AtomicInteger; Go offers sync.Mutex, sync.WaitGroup and atomic, but encourages channels to pass data instead of sharing it.
Linux kernel
The kernel uses spinlocks for very short critical sections (especially where it cannot sleep, such as interrupt handlers) and mutexes or semaphores for longer ones.
In a project
An online store occasionally sells more units than it has in stock during flash sales, because two requests both read 'stock = 1' and both succeed.
- Reproduce the race with a load test of 100 concurrent purchases of an item with stock 10.
- Make the check-and-decrement atomic in the database: UPDATE items SET stock = stock - 1 WHERE id = ? AND stock > 0, and treat 0 rows updated as sold out.
- For more complex flows, wrap the read and write in a transaction with SELECT ... FOR UPDATE on the item row.
- Keep the critical section short: do payment authorisation outside the lock and use a reservation with a timeout.
- Re-run the load test and add an alert on any negative stock.
Impact: Overselling drops to zero under the same load, with only a small increase in latency on the hottest items, and the fix works across many app servers because the database enforces it, not an in-process lock.
Trade-offs
Use it when
- Mutex: exclusive access to shared mutable data
- Semaphore: limiting concurrent use of a pool of N resources, or signalling between threads
- Atomics: single counters or flags updated very frequently
- Spinlocks: very short critical sections on multi-core systems where sleeping costs more than waiting
Avoid it when
- One big global lock around everything: safe but serialises the program
- Spinlocks for long waits: they burn CPU doing nothing
- In-process locks to protect data shared across multiple servers: use database locks or a distributed lock instead
- Locking when data can be made immutable or owned by one thread
Cost: An uncontended lock is cheap (often tens of nanoseconds); a contended lock forces waiting and context switches, so throughput falls as contention rises.
Common mistakes
- Assuming counter += 1 or 'check then act' is atomic.
- Saying a binary semaphore and a mutex are identical: a mutex has an owner that must release it; a semaphore can be signalled by any thread.
- Using 'if' instead of 'while' around a condition variable wait.
- Holding a lock while doing slow I/O, which kills throughput.
- Forgetting to release a lock on an error path, leaving every other thread stuck.
Interview questions
Quick revision
- Race condition: result depends on timing of shared access.
- Critical section: code that must not run in two threads at once.
- Mutex: one owner at a time; release in finally or with.
- Semaphore(N): up to N at once; wait/P decrements, signal/V increments.
- Condition variable: always with a mutex, always wait in a while loop.
- Atomics / compare-and-swap for single values.
- Keep critical sections short; no slow I/O while holding a lock.
- Across servers: database locks, unique constraints or distributed locks.
Check yourself
1. Two threads each run counter += 1 once, starting from 5, with no lock. What values can counter end with?
2. A service may open at most 5 connections to a database. Which primitive fits best?
3. Why should a condition variable wait be inside a while loop?
4. Which is a key difference between a mutex and a binary semaphore?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.