In this article
  1. Before you start
  2. Why interviewers ask this
  3. Concurrency in coding rounds from first principles
  4. Worked example
  5. Trade-offs and edge cases
  6. How to answer it in an interview
  7. Common mistakes
  8. Follow-up questions
  9. Sources

Before you start

This page belongs to the machine coding rounds guide, which covers the wider set of applied coding problems.

Why interviewers ask this

A thread that works alone often breaks the moment a second thread joins, so a concurrency question shows whether you can see the bug before the program does. Candidates report prompts such as a connection pool built on a blocking queue, a concurrent blocking queue, an asynchronous task scheduler, two threads printing even and odd numbers in order, and the difference between a mutex and a semaphore. These are reports from candidates, not confirmed company practice.

A weak answer adds a lock around everything, or says "Python has a lock so it is safe." A strong answer names the rule that must stay true, shows the interleaving that breaks it, picks the smallest tool that protects it, and says what happens when the pool is empty or full. Advice for the round: state the invariant out loud before you write any code.

Concurrency in coding rounds from first principles

Race condition
A race condition is a bug in which the correctness of a computation depends on the relative timing or interleaving of several threads, so the same program gives different results on different runs, for example when two threads read a counter before either writes it back.
Deadlock
A deadlock is a state in which two or more threads wait indefinitely for each other to release resources, so none of them can make progress, for example thread A holds lock one and wants lock two while thread B holds lock two and wants lock one.
Blocking queue
A blocking queue is a thread-safe queue whose put operation waits when the queue is full and whose take operation waits when it is empty, so producer threads and consumer threads coordinate without any extra signalling code.
Condition variable
A condition variable is a synchronisation object tied to a lock that lets a thread sleep until another thread signals that a shared condition may now be true, releasing the lock while it sleeps and taking it back before it continues.

In plain words

Picture two cooks sharing one notepad that tracks how many eggs are left. Each cook reads the number, thinks, then writes a new number. If both read "six" and both write "five", an egg vanished from the books. The analogy breaks in one place: real threads can be paused between any two instructions, not only when you expect it.

Concurrency means structuring a program as tasks that make progress over overlapping periods of time. Parallelism means two tasks run at the same instant on different processor cores. You can have the first without the second. Most interview problems ask for the first.

Step 1: Two threads race and break an invariant

A race condition is a bug in which the result depends on thread timing. The usual case is count += 1, which is three actions: read the counter, add one, write it back. If threads A and B both read before either writes, one increment is lost. The other case is check-then-act, such as "if the seat is free, book it", the shape behind double booking in a hotel reservation system.

Both break an invariant, a rule that must hold whenever no thread is mid-update, such as "a seat has at most one booking". In the definition of Goetz et al. (2006) in Java Concurrency in Practice, thread safety is the property that a class behaves correctly under any interleaving with no extra coordination from callers, where correctness means conforming to the class's specification. Input: two threads, one counter. Change: their reads and writes interleave. Output: a wrong total on some runs. Goetz et al. (2006) define a race condition the same way: a computation whose correctness depends on the relative timing or interleaving of threads.

Step 2: A lock makes a block of code run one thread at a time

A lock is an object that only one thread can hold at a time, so one thread at a time can change shared state, and a block of code between acquiring and releasing it runs as one uninterrupted unit. Wrap the whole invariant inside it: for check-then-act, the check and the act go in the same locked block, not two separate ones.

In Python the lock is still needed, even though the global interpreter lock (GIL) exists. The GIL is the mechanism the CPython interpreter (the reference implementation of Python, written in C) uses to assure that only one thread executes Python bytecode at a time, yet the interpreter regularly switches threads between bytecode instructions, which is why locks are also necessary in pure-Python code. Keep the locked block short and never wait on slow work inside it.

Step 3: A blocking queue hands work between threads

A blocking queue makes consumers wait while it is empty, and makes producers wait while it is full. Java's undefined is a classic bounded buffer: a fixed-size array that holds items put by producers and removed by consumers, where put on a full queue blocks and take on an empty one blocks. Python's undefined module offers a thread-safe interface for exchanging data between threads.

The Java undefined interface gives four ways to insert and remove: throw an exception, return a special value, block, or time out. It is safe with many producers and many consumers. The Java interface has no close operation, unlike Python 3.13's queue.Queue.shutdown(); the usual Java workaround is a poison pill. A poison pill is a special end-of-stream item that a producer enqueues so each consumer knows to stop. Bulk operations such as addAll are not necessarily atomic.

Step 4: A condition variable lets a thread wait for a state

A condition variable lets a thread sleep until a condition on shared state becomes true. In Python's undefined, a condition is always tied to a lock, and wait() releases that lock, blocks until another thread calls notify() or notify_all(), then re-acquires the lock before returning.

Two details trip candidates. notify() does not release the lock, so the woken thread continues only after the notifier leaves its locked block. And a wake-up does not prove the state is right, so check the condition in a while loop around wait(), never in a single if. Input: a lock and a condition. Change: the waiter sleeps, then wakes. Output: code that runs only when the state allows it.

Step 5: One lock order prevents deadlock

A deadlock needs threads that wait on each other forever. The standard case: thread A holds lock one and wants lock two, while thread B holds lock two and wants lock one. The Python glossary says deadlocks can be avoided by always acquiring multiple locks in a consistent order. In your own code, acquire locks in one consistent order to avoid deadlock, and write that order in a comment.

Coffman, Elphick and Shoshani (1971) list the conditions for deadlock as mutual exclusion, hold and wait, no preemption and circular wait. A fixed order breaks circular wait, the cheapest of these to remove. Where you cannot order the locks, take them with a timeout and back off, or hold only one lock at a time. The same idea applies to queues: a thread that puts into a full bounded queue that only it drains waits forever, which is a deadlock too.

Each concurrency tool fixes the problem the last one leavesFlow of five ideas: shared state allows a race, a lock serialises updates, a blocking queue hands off work, a condition variable waits for a state, and one lock order prevents deadlock.Shared staterace conditionLockone thread at atimeBlockingqueuewaits whenemptyConditionvariablewaits for astateLock orderno deadlockDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/concurrency-in-coding-rounds.html

Each tool fixes a problem the previous one leaves open, ending with one lock order that rules out deadlock.CC BY 4.0 — source: How do you handle concurrency in a coding interview?
Step-by-step description
  1. Two threads change shared state and break a rule.
  2. A lock lets one thread at a time in.
  3. A blocking queue hands work between threads.
  4. A thread waits for a state, releasing the lock.
  5. One fixed order rules out deadlock.
  • A race condition is a bug in which the result depends on thread timing.
  • A lock lets one thread at a time change shared state.
  • A blocking queue makes consumers wait while it is empty.
  • A condition variable wait releases the lock, blocks, and re-acquires the lock before returning.
  • Acquire locks in one consistent order to avoid deadlock.

Knowing it is half the job. Saying it is the other half.

Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.

Start Free Trial →

Worked example

Two threads each add one to a counter that starts at zero, and each add is a read then a write. Six schedules of the four steps are possible. Four of the six interleavings lose an update and end at 1. Two of the six interleavings end at the correct total of 2.1 The same model with two adds per thread has 70 schedules; a real scheduler does not let you choose the schedule, and the count grows combinatorially with every added step, so testing by repetition cannot cover them.1 At two threads of 1,000 adds each (illustrative), the correct total is 2,000, and an unlocked version can finish below that.

Four of six schedules lose an update without a lockBar chart of the final counter for the six schedules of two threads that each read then write once: two schedules end at 2, the correct total, and four end at 1.Final counter (count)012112212121221211221212211Schedule (thread per step)211112Diagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/concurrency-in-coding-rounds.html
Four of the six schedules lose an update, so a test that passes once proves little.CC BY 4.0 — source: How do you handle concurrency in a coding interview?
Text description of the diagram
  • Four of the six interleavings lose an update.
  • Two of the six interleavings end at the correct total of 2.

Deadlock has the same shape. Two threads each take two locks, and the same six schedules apply. When the threads take the locks in opposite order, four of the six schedules end in deadlock; when both take lock one first, none do.1

Four of six unlocked schedules lose an update, four of six opposite-order schedules deadlock, and fixed-rate timing differs from fixed-delay timing.
QuantityValue (computed or illustrative)
Schedules, two threads, one add each6
Schedules that end at the wrong total4
Schedules, two threads, two adds each70
Deadlocking schedules, opposite lock order4 of 6
Deadlocking schedules, one lock order0 of 6
Start times, fixed rate, period 5 seconds0, 5, 10
Start times, fixed delay 5 seconds, run takes 2 (illustrative)0, 7, 14
Results of three offer calls on a queue of capacity 2accepted, accepted, refused

The two timing rows follow the undefined rules: fixed-rate runs start at the initial delay plus multiples of the period, while fixed-delay runs wait after each run ends.1 A third offer on a full queue of capacity 2 returns a refusal instead of blocking.1

Here is a connection pool, a prompt candidates report (with a pool size picked for illustration). The free connections live in the queue itself, so taking one blocks when none are left.

import queue
import threading

class Pool:
    def __init__(self, make_conn, size):
        self._free = queue.Queue(maxsize=size)
        self._out = set()
        self._lock = threading.Lock()
        for _ in range(size):
            self._free.put(make_conn())

    def acquire(self, timeout):
        try:
            conn = self._free.get(timeout=timeout)
        except queue.Empty:
            raise TimeoutError("pool exhausted")
        with self._lock:
            self._out.add(conn)
        return conn

    def release(self, conn):
        with self._lock:
            if conn not in self._out:
                raise ValueError("connection not checked out")
            self._out.remove(conn)
        self._free.put(conn)

The queue gives the "free" state for nothing, and get with a timeout gives the exhausted-pool behaviour. The _out set rejects a release of a connection that is not checked out, so a double release cannot put a duplicate in the queue. For closing, from Python 3.13, undefined wakes blocked callers with queue.ShutDown; on older versions use a closed flag guarded by a lock plus poison pills.

The "concurrent blocking queue" prompt asks you to build the queue itself. This bounded version uses one lock, two conditions on that lock and a deque.

import threading
from collections import deque

class BoundedBlockingQueue:
    def __init__(self, cap):
        self._q, self._cap = deque(), cap
        lock = threading.Lock()
        self._not_empty = threading.Condition(lock)
        self._not_full = threading.Condition(lock)

    def put(self, item):
        with self._not_full:
            while len(self._q) == self._cap:
                self._not_full.wait()
            self._q.append(item)
            self._not_empty.notify()

    def take(self):
        with self._not_empty:
            while not self._q:
                self._not_empty.wait()
            item = self._q.popleft()
            self._not_full.notify()
            return item

There are two conditions so that a put wakes a waiting consumer and a take wakes a waiting producer, instead of waking a thread that cannot proceed. Both share one lock, so the length check and the change to the deque happen in the same locked block. The waits sit in while loops because a wake-up does not prove the queue is now non-empty or non-full: another thread may have got there first.

Trade-offs and edge cases

In Python, threads suit input/output (I/O) bound work and processes suit central processing unit (CPU)-bound (central processing unit) work. The threading documentation says threads remain appropriate for several I/O-bound tasks, because the GIL is released around blocking I/O, but only one thread runs Python code at once. Free-threaded builds can disable the GIL from Python 3.13, but not by default. For processor-bound work use multiprocessing or ProcessPoolExecutor. Threads suit I/O-bound work in Python, and processes suit CPU-bound pure-Python work.

Python threads suit waiting work, processes suit computing workTable comparing Python threads and processes by the work each suits, how memory is shared, and whether pure-Python code runs in parallel.QuestionThreadsProcessesBest forwaiting on diskor networkheavy computingMemoryshared, needslocksseparate, sendmessagesPython code in parallelno (default build)yes, one perprocessDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/concurrency-in-coding-rounds.html
In Python, pick threads for waiting on disk or network and processes for heavy computing.CC BY 4.0 — source: How do you handle concurrency in a coding interview?
Text description of the diagram
  • Threads suit I/O-bound work in Python.
  • Processes suit CPU-bound pure-Python work.

Other edge cases an interviewer may probe:

  • Fairness. A fair queue avoids starvation but lowers throughput; the default waiting order is not promised.
  • Compound operations. Each queue method is thread-safe, but "check size, then put" across two calls is still check-then-act.
  • Timing. Delayed tasks run no sooner than their time, with no promise about when they start; if one periodic run throws an exception, later runs are suppressed.
  • Stopping. Python threads cannot be stopped from outside, so design a stop signal.

For thread-safe caches see the least recently used (LRU) design in LRU cache in Python and time-to-live (TTL) caches.

How to answer it in an interview

Interviewer asks

Implement a connection pool that many threads can use safely.

A strong answer

First I state the invariant: a connection is held by at most one thread, and the pool never hands out more than its size. I keep the free connections in a bounded blocking queue, so acquire blocks when none are free and I add a timeout for an exhausted pool. Release puts the connection back and rejects one that is not checked out. To close, I call the queue's shutdown() (Python 3.13 or later) so waiters wake, or use a flag and poison pills. I would test it with many threads and a short timeout.

The spoken structure: name the invariant, pick the queue, add the timeout, then handle closing and the edge case of a double release.

Common mistakes

  • "I added a lock around the counter, so the program is thread-safe." (wrong) The invariant may span several variables or two calls. (right) "I put the check and the act inside the same locked block."
  • "Python has a GIL, so I don't need locks." (wrong) The interpreter can switch threads between bytecode instructions. (right) "The GIL protects the interpreter; my own invariants still need a lock."
  • "I call wait() inside an if." (wrong) A wake-up does not prove the condition holds. (right) "I re-check the condition in a while loop."
  • "I'll close the queue when I'm done." (wrong) A Java BlockingQueue has no close operation. (right) "In Java I enqueue a poison pill for each consumer; in Python 3.13 or later I call shutdown()."
  • "Two locks are fine in any order." (wrong) Opposite order can deadlock. (right) "All code takes lock one, then lock two."

Follow-up questions

How do you implement an asynchronous task scheduler with proper concurrency and thread safety?

Keep tasks in a thread-safe priority structure ordered by run time, and let a small pool of worker threads take the earliest task once its time arrives. Guard submit, cancel and shutdown with one lock or use a ready-made scheduled executor, and remember that a start time means "no sooner than", with no promise about when. Catch exceptions inside periodic tasks, or later runs stop.

What is the difference between a mutex and a semaphore?

A mutex is a lock held by one thread at a time, usually released by the same thread. A semaphore is a counter of permits: up to that many threads may enter, so it limits a pool or throttles access. A semaphore with one permit behaves like a mutex without the owner rule.

What are the conditions for deadlock, and how do you avoid it?

Mutual exclusion, hold and wait, no preemption and circular wait must all hold. Break one: the cheapest is a fixed lock order, which removes circular wait, as in the Python glossary. Timeouts on lock acquisition are the second option.

How do you print even and odd numbers in order from two threads?

Share one counter and one condition variable. Each thread waits in a while loop until the counter's parity is its own, prints, increments the counter, and notifies the other thread. The loop matters because a wake-up does not prove it is your turn.

Does the GIL make Python code thread-safe?

No. The GIL stops two threads running bytecode at the same instant, but the interpreter can switch between instructions, so count += 1 can still lose an update. For bigger design questions of this kind see low level design interview questions, event and log processing problems, the circuit breaker pattern and the in-memory database assessment.

Key takeaways
  • Name the invariant first: a lock protects a rule, not a variable.
  • A blocking queue gives hand-off, bounded size and waiting for free; Java's has no close, so use poison pills, while Python 3.13 adds shutdown().
  • Call wait() in a loop, and remember notify() does not release the lock.
  • Take locks in one fixed order to rule out deadlock.
  • In Python, threads help with waiting, processes help with computing, and the GIL does not replace locks.

Sources

  1. threading: Thread-based parallelism (Python documentation) — Python Software Foundation, 26 Oct 2026
  2. Python glossary: deadlock and global interpreter lock — Python Software Foundation, 26 Oct 2026
  3. Thread states and the global interpreter lock (Python C API) — Python Software Foundation, 26 Oct 2026
  4. BlockingQueue (Java SE 17 API) — Oracle, 26 Oct 2026
  5. ArrayBlockingQueue (Java SE 14 API) — Oracle, 26 Oct 2026
  6. ScheduledThreadPoolExecutor (Java SE 11 API) — Oracle, 26 Oct 2026
  7. queue: A synchronized queue class (Python documentation) — Python Software Foundation, 26 Oct 2026
  8. System Deadlocks (Coffman, Elphick and Shoshani), ACM Computing Surveys — ACM, 1 Dec 1971
  9. What is thread safety? (quotes Java Concurrency in Practice) — HowToDoInJava (secondary), 26 Oct 2026
  10. Concurrency: thread safety in Java (quotes Java Concurrency in Practice) — DEV Community (secondary), 26 Oct 2026