Concurrency Patterns

The three bugs of shared state (races, deadlocks, stale reads) and the patterns that prevent them: locks, thread pools, bounded queues, read-write locks and lock ordering.

Overview

Concurrency questions in a low-level design interview come in one shape: here is some shared state, several threads touch it, make it correct. The answer is never 'add a lock everywhere'. It is naming the bug that can happen, picking the pattern that rules it out, and knowing what the language guarantees.

This topic uses Python and Java. Python examples run; Java examples compile. The guarantees come from the Java Language Specification and the Python and Java library documentation, and each one is cited.

The three bugs

  • Race condition (lost update) — Two threads read the same value, both compute, both write; one write is lost — Two increments of a counter produce one
  • Deadlock — Two threads each hold a lock the other needs; both wait forever — Transfer A to B while another thread transfers B to A
  • Visibility (stale read) — One thread writes a field, another keeps reading the old value from a register or cache — A stop flag that the worker never sees

The patterns

  • Mutual exclusion — Race — synchronized, ReentrantLock — threading.Lock, RLock
  • Atomic variables — Race on one number or reference — AtomicInteger, AtomicReference — No direct equivalent; use a lock
  • Thread pool — Unbounded thread creation — ThreadPoolExecutor — concurrent.futures.ThreadPoolExecutor
  • Producer and consumer with a bounded queue — Fast producers overrunning slow consumers — ArrayBlockingQueue — queue.Queue(maxsize)
  • Read-write lock — Readers blocking each other for no reason — ReentrantReadWriteLock — Build one from a Condition
  • Lock ordering — Deadlock — Any locks — Any locks
  • Immutability — Every bug at once, for data that never changes — final fields, records — frozen dataclasses, tuples

Thread pool

Creating a thread costs a stack (the JVM reserves 1 MB per thread by default on Linux/x64 and macOS/x64, 2 MB on Aarch64) and a kernel object. A server that starts one thread per request can run out of memory under load. A pool creates a fixed set of workers once and feeds them from a queue, so the number of threads is bounded and the queue absorbs bursts.

Java's ThreadPoolExecutor makes the knobs explicit; the table is from its documentation.

  • corePoolSize — Threads kept alive even when idle
  • maximumPoolSize — Upper bound; extra threads are created only when the queue is full
  • Work queue — Direct handoff (SynchronousQueue), unbounded (LinkedBlockingQueue with no capacity) or bounded (ArrayBlockingQueue)
  • Rejection policy — AbortPolicy (default, throws), CallerRunsPolicy (the submitter runs it, which slows submission), DiscardPolicy, DiscardOldestPolicy

With an unbounded queue, maximumPoolSize never matters and the queue can grow without limit. That is the trap interviewers ask about.

Producer and consumer

A bounded blocking queue does the coordination for you: put blocks when the queue is full, get blocks when it is empty, and the queue's own lock makes both safe. Python's queue.Queue and Java's BlockingQueue document exactly that.

Read-write lock

A mutex lets one thread in at a time, readers included. A read-write lock lets any number of readers in together and gives a writer exclusive access. The Java documentation says the gain is real only when the structure is large, readers outnumber writers, and the work done under the lock outweighs the cost of the lock itself. It also warns that a read lock cannot be upgraded to a write lock; release the read lock, take the write lock, and check again.

  • Configuration read on every request, rewritten once an hour — Read-write lock
  • A counter incremented by every request — Mutex or an atomic
  • An LRU cache — Mutex: every get moves an item, so reads are writes
  • Data that never changes after startup — No lock; make it immutable

Lock ordering

A deadlock needs a cycle: thread 1 holds A and wants B, thread 2 holds B and wants A. If every thread takes locks in the same global order, no cycle can form. When the locks come from data (two accounts in a transfer), sort them by a stable key such as the account id.

What you will be able to do

  • Name the bug an interviewer's scenario contains (race, deadlock, visibility) before proposing a fix.
  • Write a thread-safe counter and a bounded blocking queue from a lock and a condition variable, and test them.
  • Explain thread pool sizing and what happens when the queue is full.
  • Show a deadlock with two locks and remove it with lock ordering.
  • Say what volatile, synchronized and atomics each guarantee in Java, and what the GIL does and does not do in Python.

Interview questions

  • What is a race condition? Show one and fix it.
  • Show a deadlock with two locks and fix it.
  • How do you size a thread pool, and what happens when it is full?
  • Design a bounded blocking queue from scratch.
  • What do volatile, synchronized and atomics guarantee in Java, and what does the GIL guarantee in Python?

Sign up free to read the full topic

More Low-Level Design topics

Start free on Cariara · Coding problems · System design studio