Free lesson · Operating Systems
What's wrong?
How Synchronization Questions Appear in Interviews
Synchronization questions in FAANG interviews come in four forms. The first is conceptual - "explain what a mutex is" or "what is the difference between a semaphore and a condition variable." The second is bug identification - "what is wrong with this concurrent code." The third is design - "design a thread-safe data structure" or "how would you prevent deadlock in this system." The fourth is system design connection - "how does your web server handle concurrent requests" or "how does the database prevent lost updates."
This topic prepares you for all four forms by showing you the exact patterns that appear, the structured answers interviewers expect, and the common follow-up questions that separate strong candidates from average ones.
Pattern 1: "Explain the Difference Between X and Y"
These are the warm-up questions. They test whether you have a precise mental model or just memorized definitions. The key to a strong answer is to state the core difference in one sentence, then support it with a practical implication.
Mutex vs Semaphore
The core difference: A mutex provides mutual exclusion with ownership - the thread that locks it must unlock it. A semaphore is a counter with no ownership - any thread can signal it.
The practical implication: Mutexes are for protecting shared data (one writer at a time). Counting semaphores are for limiting concurrency (at most N threads). Binary semaphores are for signaling between threads (one thread waits, another signals).
The follow-up you should anticipate: "Can you implement a mutex using a semaphore?" Yes - a binary semaphore initialized to 1 behaves like a mutex for mutual exclusion, though without ownership semantics, recursive locking, or priority inheritance.
Semaphore vs Condition Variable
The core difference: A semaphore's signal accumulates (incrementing a counter). A condition variable's signal is lost if no thread is waiting (it does not accumulate). A semaphore is self-contained. A condition variable is always paired with a mutex.
The practical implication: Semaphores work well for counting problems where permits represent concrete resources. Condition variables work well for arbitrary boolean conditions that require evaluating shared state under a lock.
The follow-up: "Why must you always use a while loop with condition variables but not with semaphores?" Because condition variables use Mesa semantics - the condition might change between the signal and the wakeup (stolen wakeups, spurious wakeups). A semaphore's wait is guaranteed to consume exactly one permit, so no re-check is needed.
Process vs Thread Synchronization
The core difference: Threads within the same process share an address space, so they can use in-memory mutexes, semaphores, and condition variables directly. Processes have separate address spaces, so they need either shared memory (with synchronization primitives placed in the shared region) or kernel-mediated synchronization (POSIX named semaphores, file locks, pipes).
The practical implication: Thread synchronization is faster (no system calls for user-space locks like futexes in the fast path). Process synchronization is more robust (a crashed process does not corrupt another process's memory, but a crashed thread can corrupt the entire process).
Deadlock vs Livelock vs Starvation
The core difference: Deadlock - threads are blocked forever, waiting for each other in a cycle. Nobody makes progress. Livelock - threads are active but keep undoing each other's work. They are running but not making progress (like two people stepping side to side in a hallway). Starvation - one thread makes progress while another never gets a chance, even though it is ready to run.
The practical implication: Deadlock is detected by finding cycles in the resource-allocation graph. Livelock is harder to detect - the system is active but throughput is zero. Starvation is prevented by fairness mechanisms (FIFO queues, aging).
Pattern 2: "What Is Wrong with This Code?"
Bug identification questions give you concurrent code and ask you to find the synchronization error. Here are the four most common bugs and how to spot them.
Bug 1: Missing Lock
# What's wrong?
counter = 0
def increment():
global counter
counter += 1 # Not protected - race condition
What to say: "counter += 1 is a read-modify-write operation that compiles to three instructions (load, add, store). Without a lock, two threads can interleave these instructions and lose updates. The fix is to wrap the increment in a mutex, or use an atomic operation."
Bug 2: Lock Scope Too Narrow
# What's wrong?
lock = threading.Lock()
def transfer(from_acct, to_acct, amount):
with lock:
balance = from_acct.balance
# Lock released here - another thread can modify from_acct.balance
with lock:
from_acct.balance = balance - amount
to_acct.balance += amount
What to say: "The lock is acquired and released between the read and the write. Between those two critical sections, another thread can modify from_acct.balance, making the read value stale. The entire read-modify-write sequence must be under a single lock acquisition."
Bug 3: Deadlock from Lock Ordering
# What's wrong?
def transfer(from_acct, to_acct, amount):
from_acct.lock.acquire()
to_acct.lock.acquire()
# ... transfer ...
to_acct.lock.release()
from_acct.lock.release()
# Thread A: transfer(account_1, account_2, 100)
# Thread B: transfer(account_2, account_1, 50)
# Thread A holds account_1's lock, waits for account_2's lock
# Thread B holds account_2's lock, waits for account_1's lock
# DEADLOCK
What to say: "Two threads acquiring the same two locks in opposite order creates a circular wait - one of the four Coffman conditions for deadlock. The fix is lock ordering: always acquire locks in a consistent order, such as by account ID. The thread that needs accounts 1 and 2 always locks account 1 first, regardless of which is the source and which is the destination."
# FIXED: consistent lock ordering
def transfer(from_acct, to_acct, amount):
first, second = sorted([from_acct, to_acct], key=lambda a: a.id)
first.lock.acquire()
second.lock.acquire()
# ... transfer ...
second.lock.release()
first.lock.release()
Bug 4: Condition Variable with if Instead of while
// What's wrong?
synchronized void consume() throws InterruptedException {
if (queue.isEmpty()) { // BUG: should be while
wait();
}
Item item = queue.remove(); // Might throw - queue could be empty!
}
What to say: "Using if instead of while with a condition variable is a bug. After wait() returns, the condition might be false - either due to a spurious wakeup, or because another thread consumed the item between the signal and this thread re-acquiring the monitor. The while loop re-checks the condition after every wakeup."
Interview: General strategy for "find the bug" questions:
- Identify the shared state. What data is accessed by multiple threads?
- Check for missing synchronization. Is every access to shared state protected?
- Check lock scope. Does the lock cover the entire logical operation, or just individual accesses?
- Check for deadlock. Are multiple locks acquired? If so, is the order consistent?
- Check condition variable discipline. Is
waitin a while loop? Is the mutex held?Walk the interviewer through these steps explicitly. Showing your systematic approach matters as much as finding the bug.
Pattern 3: "Design a Thread-Safe X"
Design questions ask you to build a concurrent data structure or system component. The approach is always the same: start with the single-threaded version, identify the shared state, then add synchronization.
graph TD
S1["Step 1: Define the interface<br/>What operations does it support?"] --> S2["Step 2: Identify shared state<br/>What data do multiple threads touch?"]
S2 --> S3["Step 3: Choose synchronization<br/>Mutex? Monitor? RWLock? Lock-free?"]
S3 --> S4["Step 4: Implement<br/>Add locks, CVs, or atomics"]
S4 --> S5["Step 5: Discuss tradeoffs<br/>Granularity, performance, alternatives"]
style S1 fill:#4f46e5,stroke:#4338ca,color:#fff
style S2 fill:#4f46e5,stroke:#4338ca,color:#fff
style S3 fill:#4f46e5,stroke:#4338ca,color:#fff
style S4 fill:#4f46e5,stroke:#4338ca,color:#fff
style S5 fill:#4f46e5,stroke:#4338ca,color:#fff
This diagram shows the five-step approach for designing a thread-safe data structure in an interview. Step 1 is defining the interface and the operations it supports. Step 2 is identifying the shared state that multiple threads access. Step 3 is choosing the right synchronization mechanism such as a mutex, monitor, read-write lock, or lock-free approach. Step 4 is implementing the solution by adding locks, condition variables, or atomic operations. Step 5 is discussing tradeoffs including lock granularity, performance characteristics, and alternative approaches.
Example: Thread-Safe Bounded Queue
Step 1: Define the interface.
class ThreadSafeQueue:
def put(self, item): # Add item - block if full
...
def get(self): # Remove item - block if empty
...
def size(self): # Current number of items
...
Step 2: Identify shared state. The internal list/array, the count, and the read/write indices are all shared.
Step 3: Choose synchronization. A monitor (mutex + condition variables) is the cleanest choice. Two conditions: "not full" for producers, "not empty" for consumers.
Step 4: Implement with synchronization.
import threading
class ThreadSafeQueue:
def __init__(self, capacity):
self._queue = []
self._capacity = capacity
self._lock = threading.Condition()
def put(self, item):
with self._lock:
while len(self._queue) >= self._capacity:
self._lock.wait()
self._queue.append(item)
self._lock.notify_all()
def get(self):
with self._lock:
while len(self._queue) == 0:
self._lock.wait()
item = self._queue.pop(0)
self._lock.notify_all()
return item
def size(self):
with self._lock:
return len(self._queue)
# Quick verification
q = ThreadSafeQueue(3)
results = []
def producer():
for i in range(5):
q.put(i)
def consumer():
for i in range(5):
results.append(q.get())
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Results: {sorted(results)}")
print(f"All items received: {sorted(results) == [0,1,2,3,4]}")
Step 5: Discuss tradeoffs. After implementing, discuss:
- "I used
notify_all()which wakes all waiters. For better performance, I could use two separate condition variables - one for producers and one for consumers - and usenotify()to wake only the relevant waiter." - "This uses a single lock, which serializes all access. For higher concurrency, I could use a lock-free implementation with CAS, but that is significantly more complex."
- "The
pop(0)is O(n) for a list. In production, I would usecollections.dequefor O(1) pops from the left, or a circular buffer with head/tail indices."
Example: Design a ReadWriteLock
The interview approach:
"I would maintain three pieces of state: a reader count, a writer-active flag, and a writer-waiting flag. All three are protected by a mutex, and I would use two condition variables - one for readers to wait on and one for writers to wait on.
read_lock(): Acquire the mutex. While a writer is active or a writer is waiting (writers-preferred policy), wait on the readers' condition variable. Increment the reader count. Release the mutex.
read_unlock(): Acquire the mutex. Decrement the reader count. If the count reaches zero, signal the writers' condition variable - a waiting writer can now proceed. Release the mutex.
write_lock(): Acquire the mutex. Set writer-waiting to true. While a writer is active or the reader count is greater than zero, wait on the writers' condition variable. Set writer-active to true, writer-waiting to false. Release the mutex.
write_unlock(): Acquire the mutex. Set writer-active to false. Signal the writers' condition variable (for a waiting writer) and broadcast the readers' condition variable (for waiting readers). Release the mutex."
Then discuss: "This is writers-preferred because new readers block while a writer is waiting. For readers-preferred, remove the writer-waiting check from read_lock(). For FIFO fairness, use a single queue rather than separate conditions."
Pattern 4: System Design Connections
In system design interviews, synchronization questions appear as sub-problems within larger designs. You are not asked to implement a mutex - you are asked how your system handles concurrent access, and the answer requires demonstrating that you understand synchronization at a design level.
"How does your database handle concurrent writes to the same row?"
Strong answer structure:
"There are two main approaches - pessimistic locking and optimistic locking.
Pessimistic locking acquires an exclusive lock on the row before reading it. No other transaction can read or write the row until the lock is released. This is like a mutex on the row. It guarantees consistency but reduces concurrency - other transactions are blocked even if they would not conflict.
Optimistic locking does not lock the row. Instead, each row has a version number. When a transaction reads the row, it records the version. When it writes, it checks that the version has not changed. If it has, the write fails and the transaction retries. This is like a compare-and-swap on the version field. It allows more concurrency but adds retry overhead when conflicts occur.
Most production databases also support MVCC (Multi-Version Concurrency Control), where readers see a snapshot of the data at their transaction's start time. Readers never block writers, and writers never block readers. Only writer-writer conflicts are detected. PostgreSQL and MySQL InnoDB both use MVCC."
"How does your web server handle 10,000 concurrent requests?"
Synchronization relevance: The thread pool is a producer-consumer system. The main thread accepts connections (producer) and adds them to a task queue. Worker threads (consumers) take connections from the queue and handle them. The queue is a bounded buffer protected by a mutex and condition variables.
"I would use a thread pool with a bounded task queue. The main thread accepts connections and adds them to the queue. Worker threads wait on a condition variable for new tasks. If the queue is full, the main thread either blocks (back-pressure) or rejects the connection (load shedding). The queue size and thread count are tuning parameters - too few threads underutilize the CPU, too many threads cause excessive context switching."
"What happens if two users try to buy the last item in stock?"
Synchronization relevance: This is the check-then-act race condition from Topic 2.
"Without proper synchronization, both users check the stock count, see 1 item available, and both proceed to purchase - creating an oversell. The fix is to use an atomic operation: UPDATE inventory SET count = count - 1 WHERE product_id = X AND count > 0. This is a compare-and-swap at the database level - the decrement only succeeds if the count is still positive. Only one of the two concurrent requests will succeed."
graph TD
subgraph PATTERNS["Four Interview Question Patterns"]
P1["Pattern 1: Explain X vs Y<br/>Mutex vs Semaphore<br/>Deadlock vs Livelock"]
P2["Pattern 2: Find the Bug<br/>Missing lock, wrong scope,<br/>deadlock, if vs while"]
P3["Pattern 3: Design Thread-Safe X<br/>Bounded queue, RWLock,<br/>thread pool"]
P4["Pattern 4: System Design Connection<br/>Database concurrency,<br/>server architecture"]
end
P1 -->|"tests"| K1["Vocabulary and<br/>precise definitions"]
P2 -->|"tests"| K2["Bug pattern recognition<br/>and systematic analysis"]
P3 -->|"tests"| K3["Implementation skill<br/>and tradeoff reasoning"]
P4 -->|"tests"| K4["Architectural understanding<br/>and real-world application"]
This diagram shows the four patterns of synchronization interview questions. Pattern 1, explaining the difference between concepts, tests vocabulary and precise definitions. Pattern 2, finding bugs in concurrent code, tests bug pattern recognition and systematic analysis. Pattern 3, designing thread-safe data structures, tests implementation skill and tradeoff reasoning. Pattern 4, system design connections, tests architectural understanding and real-world application of synchronization concepts.
Quick Reference: What to Know Cold
These are the facts and comparisons you should be able to state without hesitation:
The Four Coffman Conditions for Deadlock
- Mutual exclusion - resources are non-shareable
- Hold and wait - thread holds one resource while waiting for another
- No preemption - resources cannot be forcibly taken
- Circular wait - cycle of threads waiting for each other
Break any one to prevent deadlock. In practice, resource ordering (breaking circular wait) is the most common strategy.
The Three Requirements for Critical Section Solutions
- Mutual exclusion - only one thread in the critical section at a time
- Progress - if the critical section is free, some waiting thread must be able to enter
- Bounded waiting - no thread waits forever
Synchronization Primitive Comparison
| Primitive | What It Does | Ownership | Signals Accumulate | Wait Requires Lock | Best For |
|---|---|---|---|---|---|
| Mutex | One-at-a-time access | Yes | N/A | N/A | Protecting shared data |
| Binary Semaphore | One-at-a-time or signaling | No | Yes | No | Thread signaling |
| Counting Semaphore | At-most-N access | No | Yes | No | Resource pools, rate limiting |
| Condition Variable | Wait for arbitrary condition | N/A | No | Yes | Complex state conditions |
| Monitor | Structured mutex + CVs | Implicit | No | Implicit | Thread-safe classes |
| Read-Write Lock | Many readers OR one writer | Partial | N/A | N/A | Read-heavy workloads |
Performance Numbers for Interviews
| Operation | Approximate Cost |
|---|---|
| Uncontended mutex lock/unlock | 25-100 ns |
| Contended mutex (context switch) | 2-20 μs |
| Spinlock (short hold) | 10-100 ns |
| Semaphore wait/signal (uncontended) | 50-200 ns |
| CAS instruction | 5-50 ns |
| Context switch | 1-10 μs |
| Thread creation | 10-50 μs |
Interview: Final preparation checklist for synchronization interviews:
Can you define mutual exclusion, progress, bounded waiting, deadlock, livelock, starvation, race condition, data race, critical section?
Can you explain the difference between mutex/semaphore, semaphore/condition variable, Hoare/Mesa monitors, spinlock/mutex, pessimistic/optimistic locking?
Can you implement a thread-safe bounded queue using condition variables? A simple spinlock using test-and-set? A read-write lock using a mutex and condition variables?
Can you identify the bug in concurrent code - missing locks, wrong lock scope, deadlock from lock ordering, if-instead-of-while on condition variables?
Can you connect synchronization concepts to system design - how your database handles concurrent writes, how your web server handles concurrent requests, how you prevent the lost-update problem in your application?
If you can do all five, you are well prepared for synchronization questions at any level.
Exercise: Starter: Without looking at notes, write down the four Coffman conditions for deadlock and the three requirements for a correct critical section solution. For each Coffman condition, name one practical strategy to break it. Check your answers against the quick reference above.
Intermediate: Given this code, identify all synchronization bugs and fix them:
import threading accounts = {"A": 1000, "B": 1000} lock_a = threading.Lock() lock_b = threading.Lock() def transfer_a_to_b(amount): lock_a.acquire() lock_b.acquire() if accounts["A"] >= amount: accounts["A"] -= amount accounts["B"] += amount lock_b.release() lock_a.release() def transfer_b_to_a(amount): lock_b.acquire() lock_a.acquire() if accounts["B"] >= amount: accounts["B"] -= amount accounts["A"] += amount lock_a.release() lock_b.release()Advanced: Design and implement a thread-safe LRU cache in Python. The cache should support
get(key)andput(key, value)with a fixed capacity. When the cache is full and a new item is added, the least recently used item is evicted. Bothgetandputshould be thread-safe using a mutex. Then optimize: is a single global lock the best approach, or could you use finer-grained locking? What are the tradeoffs? Implement both versions and compare their behavior under concurrent access from 10 threads.