← All articles
13 min read

Concurrency and Multithreading Interview Questions (2026)

Concurrency rounds test whether you can reason about shared state, not whether you memorized API names. This guide covers the core concepts and gives working Java and Python solutions to the four classic problems interviewers ask.

Concurrency and multithreading interview questions test whether you can reason about shared state when several threads run at once. Expect concept questions (threads vs processes, mutex vs semaphore, deadlock) plus at least one coding problem such as a producer-consumer queue or a bounded blocking queue. The candidates who pass can explain why their code is correct under every interleaving, not just show that it ran once.

This guide covers the language-agnostic concepts first, then gives working Java and Python implementations of the four problems interviewers ask most: producer-consumer, bounded blocking queue, dining philosophers, and a rate-limited worker.

Key Takeaways

  • Every concurrency bug comes from one of three causes: unsynchronized access to shared mutable state, waiting in the wrong order (deadlock), or waiting on the wrong thing (missed or spurious wakeups).
  • Always wait on a condition variable inside a while loop, and always release locks in a finally block or a with statement.
  • Lock ordering is the deadlock fix interviewers expect first. Name the Coffman condition you are breaking.
  • Python's GIL does not make counter += 1 safe. Java's volatile does not make count++ safe either.
  • Know when to reach for a library primitive (BlockingQueue, queue.Queue, Semaphore) and when the interviewer wants you to build it from a lock and conditions.

Threads vs Processes: What Is the Difference?

A process is an independent program with its own address space. A thread is a unit of execution inside a process that shares memory with the other threads in that process. Sharing memory makes threads cheap to communicate between and dangerous to get wrong.

AspectProcessThread
MemorySeparate address spaceShared heap, private stack
Creation costHigherLower
CommunicationPipes, sockets, shared memory segmentsDirect shared variables
Failure isolationA crash usually stays containedOne bad thread can corrupt or kill the process
Typical useIsolation, CPU parallelism in PythonConcurrent work on shared data

Two follow-ups come up often. First, concurrency is about dealing with many tasks at once (interleaving), while parallelism is about running tasks at the same instant on multiple cores. Second, in Python the global interpreter lock limits CPU parallelism for threads in the default CPython build. CPython 3.13 shipped an experimental free-threaded build, and 3.14 moved it to officially supported but still optional status, so "it depends on the build" is now the accurate answer. Our Python interview questions guide covers the GIL in more depth.

What Is a Race Condition?

A race condition is a bug where the result depends on the timing of thread execution. The classic example is an unsynchronized counter.

class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }
}

count++ is three steps: read, add, write. Two threads can both read 5 and both write 6, losing an update. The fix is to make the read-modify-write atomic with synchronized, a ReentrantLock, or AtomicInteger.incrementAndGet().

The Python version has the same bug, GIL or not:

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    with lock:
        counter += 1

Interviewers also probe two related ideas:

  • Critical section: the code that touches shared state and must run in one thread at a time.
  • Visibility: in Java, one thread's write may not be seen by another without a happens-before relationship. volatile fixes visibility for a single variable but not atomicity of compound operations. Per the Java Language Specification, unlocking a monitor happens-before every subsequent lock of that same monitor, which is why synchronized fixes both.

A check-then-act sequence is the subtler variant. if (!map.containsKey(k)) map.put(k, v) is racy even on a ConcurrentHashMap, because the two calls are separately atomic. Use putIfAbsent or computeIfAbsent.

Mutex vs Semaphore vs Condition Variable

These three primitives answer different questions. A mutex asks "who may touch this data?" A semaphore asks "how many may proceed?" A condition variable asks "when should a waiting thread wake up?"

PrimitiveWhat it isUse it forJavaPython
Mutex (lock)Exclusive lock with one ownerProtecting shared statesynchronized, ReentrantLockthreading.Lock, RLock
SemaphoreCounter of N permits, any thread can releaseCapping concurrency, signalingjava.util.concurrent.Semaphorethreading.Semaphore, BoundedSemaphore
Condition variableWait queue tied to a lockWaiting until a predicate becomes trueCondition, wait/notifythreading.Condition
Read-write lockMany readers or one writerRead-heavy shared dataReentrantReadWriteLockNot in stdlib

A binary semaphore looks like a mutex but differs in ownership. A mutex should be released by the thread that acquired it. A semaphore can be released by any thread, which makes it useful for signaling between threads (thread A finishes, releases, thread B proceeds).

A reentrant lock lets the thread that already holds it acquire it again without blocking. Java's synchronized and ReentrantLock are reentrant. Python's Lock is not, so recursive code that re-acquires a plain Lock deadlocks on itself; use RLock there.

Deadlock: How Does It Happen and How Do You Avoid It?

Deadlock is a state where two or more threads each wait for a resource another holds, so none can proceed. It requires all four Coffman conditions:

  1. Mutual exclusion: a resource is held by one thread at a time.
  2. Hold and wait: a thread holds one resource while requesting another.
  3. No preemption: a resource cannot be taken away from its holder.
  4. Circular wait: a cycle of threads, each waiting on the next.

Break any one and deadlock cannot occur. Here is how each maps to a practical fix:

Condition brokenTechniqueExample
Circular waitGlobal lock orderingAlways lock the account with the lower ID first
Hold and waitAcquire all locks at once or noneTry-lock both, release both on failure
No preemptionTimed lock attemptstryLock(50, TimeUnit.MILLISECONDS) then back off
Mutual exclusionAvoid shared locks entirelyMessage passing, immutable data, per-thread state

The bank transfer question is the standard example. transfer(a, b) locks a then b, while transfer(b, a) locks b then a. Run both concurrently and they deadlock. Order the locks by account ID and the cycle disappears.

Know the neighbors too. Livelock is when threads keep reacting to each other and make no progress, such as two tryLock loops that back off in lockstep (add random jitter). Starvation is when a thread never gets the resource because others keep winning; fair locks (new ReentrantLock(true)) trade throughput for ordering.

To diagnose a deadlock in Java, take a thread dump with jstack or jcmd <pid> Thread.print; the JVM reports monitor deadlocks it finds. In Python, faulthandler.dump_traceback() prints every thread's stack.

Concurrency questions are where strong engineers freeze: one missed while loop or lock-order bug and the round goes sideways. TechScreen is an invisible AI interview assistant that helps you reason through interleavings and spot deadlocks in real time during CoderPad, HackerRank, and Zoom rounds. Start with 3 free tokens, no credit card.

Get started free →

Classic Concurrency Coding Problems

These four problems cover most of what interviewers ask. For each, know the library answer and the build-it-yourself answer.

Producer-Consumer

Producer-consumer is a pattern where producer threads put work into a shared buffer and consumer threads take it out, with the buffer decoupling their speeds. The library answer uses a blocking queue and a sentinel (poison pill) to signal shutdown.

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProducerConsumer {
    private static final int POISON = -1;

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 100; i++) {
                    queue.put(i);
                }
                queue.put(POISON);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    int item = queue.take();
                    if (item == POISON) {
                        break;
                    }
                    System.out.println("consumed " + item);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        producer.start();
        consumer.start();
        producer.join();
        consumer.join();
    }
}
import queue
import threading

SENTINEL = object()

def producer(q, n, consumers):
    for i in range(n):
        q.put(i)
    for _ in range(consumers):
        q.put(SENTINEL)

def consumer(q):
    while True:
        item = q.get()
        if item is SENTINEL:
            break
        print("consumed", item)

q = queue.Queue(maxsize=10)
workers = [threading.Thread(target=consumer, args=(q,)) for _ in range(3)]
for w in workers:
    w.start()
producer(q, 100, len(workers))
for w in workers:
    w.join()

The detail interviewers check: with multiple consumers, send one sentinel per consumer. Also note the Java code restores the interrupt flag instead of swallowing InterruptedException.

Bounded Blocking Queue

This is LeetCode 1188 and the most common "build the primitive yourself" question. The pattern is one lock and two condition variables, notFull and notEmpty.

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedBlockingQueue<T> {
    private final Deque<T> items = new ArrayDeque<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BoundedBlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public void enqueue(T item) throws InterruptedException {
        lock.lock();
        try {
            while (items.size() == capacity) {
                notFull.await();
            }
            items.addLast(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public T dequeue() throws InterruptedException {
        lock.lock();
        try {
            while (items.isEmpty()) {
                notEmpty.await();
            }
            T item = items.removeFirst();
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }

    public int size() {
        lock.lock();
        try {
            return items.size();
        } finally {
            lock.unlock();
        }
    }
}
import threading
from collections import deque

class BoundedBlockingQueue:
    def __init__(self, capacity):
        self.capacity = capacity
        self.items = deque()
        lock = threading.Lock()
        self.not_full = threading.Condition(lock)
        self.not_empty = threading.Condition(lock)

    def enqueue(self, item):
        with self.not_full:
            while len(self.items) == self.capacity:
                self.not_full.wait()
            self.items.append(item)
            self.not_empty.notify()

    def dequeue(self):
        with self.not_empty:
            while not self.items:
                self.not_empty.wait()
            item = self.items.popleft()
            self.not_full.notify()
            return item

    def size(self):
        with self.not_full:
            return len(self.items)

Two separate conditions let you wake only the side that can make progress, so signal/notify (wake one) is enough. If you use Java's single built-in monitor with wait/notify, producers and consumers share one wait set, and you must use notifyAll to avoid waking the wrong kind of thread and stalling.

Dining Philosophers

Five philosophers sit around a table with five forks, and each needs both adjacent forks to eat. If everyone picks up the left fork first, all five hold one fork and wait forever. This is LeetCode 1226, and the expected fix is resource ordering: always pick up the lower-numbered fork first, which breaks circular wait.

import java.util.concurrent.locks.ReentrantLock;

public class DiningPhilosophers {
    private final ReentrantLock[] forks = new ReentrantLock[5];

    public DiningPhilosophers() {
        for (int i = 0; i < 5; i++) {
            forks[i] = new ReentrantLock();
        }
    }

    public void wantsToEat(int philosopher, Runnable pickLeftFork, Runnable pickRightFork,
                           Runnable eat, Runnable putLeftFork, Runnable putRightFork)
            throws InterruptedException {
        int left = philosopher;
        int right = (philosopher + 1) % 5;
        int first = Math.min(left, right);
        int second = Math.max(left, right);
        forks[first].lock();
        forks[second].lock();
        try {
            pickLeftFork.run();
            pickRightFork.run();
            eat.run();
            putLeftFork.run();
            putRightFork.run();
        } finally {
            forks[second].unlock();
            forks[first].unlock();
        }
    }
}
import threading

class DiningPhilosophers:
    def __init__(self):
        self.forks = [threading.Lock() for _ in range(5)]

    def wantsToEat(self, philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):
        left, right = philosopher, (philosopher + 1) % 5
        first, second = sorted((left, right))
        with self.forks[first], self.forks[second]:
            pickLeftFork()
            pickRightFork()
            eat()
            putLeftFork()
            putRightFork()

Mention the alternatives to show range: a Semaphore(4) that lets at most four philosophers reach for forks (one can always finish), or making odd and even philosophers pick up forks in opposite orders.

Rate-Limited Worker

The rate-limited worker is a practical favorite at API-heavy companies: process a list of jobs with a pool of threads, but never exceed N calls per second to a downstream service. The core is a thread-safe token bucket. It is the single-machine version of the problem in our rate limiter system design guide.

import threading
import time
from concurrent.futures import ThreadPoolExecutor

class TokenBucket:
    def __init__(self, rate, capacity):
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity
        self.last = time.monotonic()
        self.cond = threading.Condition()

    def acquire(self):
        with self.cond:
            while True:
                now = time.monotonic()
                self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
                self.last = now
                if self.tokens >= 1:
                    self.tokens -= 1
                    return
                self.cond.wait((1 - self.tokens) / self.rate)

bucket = TokenBucket(rate=5, capacity=5)

def call_api(job):
    bucket.acquire()
    return fetch(job)

with ThreadPoolExecutor(max_workers=8) as pool:
    results = list(pool.map(call_api, jobs))
import java.util.concurrent.TimeUnit;

public class TokenBucket {
    private final double ratePerNano;
    private final double capacity;
    private double tokens;
    private long last = System.nanoTime();

    public TokenBucket(double perSecond, double capacity) {
        this.ratePerNano = perSecond / 1_000_000_000.0;
        this.capacity = capacity;
        this.tokens = capacity;
    }

    public void acquire() throws InterruptedException {
        while (true) {
            long waitNanos;
            synchronized (this) {
                long now = System.nanoTime();
                tokens = Math.min(capacity, tokens + (now - last) * ratePerNano);
                last = now;
                if (tokens >= 1) {
                    tokens -= 1;
                    return;
                }
                waitNanos = (long) Math.ceil((1 - tokens) / ratePerNano);
            }
            TimeUnit.NANOSECONDS.sleep(waitNanos);
        }
    }
}

Submit jobs to Executors.newFixedThreadPool(8) and call bucket.acquire() at the start of each task. Two talking points earn credit here. The Java version sleeps outside the lock, so waiting threads do not block others from refilling. The Python version gets the same effect because Condition.wait releases the lock while waiting. Both use a monotonic clock, never wall-clock time, which can jump. If the interviewer also wants a cap on in-flight calls (not just calls per second), add a Semaphore.

Async vs Threads: Which Should You Use?

Async (coroutines with an event loop) and threads both give concurrency, but they switch tasks differently. Threads are preempted by the OS at any instruction. Coroutines switch only at explicit await points, so code between awaits runs without interruption on a single thread.

FactorThreadsAsync / coroutines
SwitchingPreemptive, anywhereCooperative, at await
Cost per taskOS thread (virtual threads are much cheaper)Small object on the heap
Best forBlocking libraries, CPU parallelism where supportedMany concurrent I/O tasks
Main hazardRaces and deadlocksBlocking the event loop
CPU-bound workFine in Java and Go; limited by GIL in default CPythonPoor fit; offload to a pool

In Java, virtual threads became a final feature in JDK 21 and give thread-per-request code most of the scalability of async without colored functions. JDK 24 (JEP 491) removed most pinning when a virtual thread blocks inside synchronized, which used to be a common interview follow-up. In Python, asyncio is the standard choice for high-concurrency I/O:

import asyncio

async def fetch_all(urls, limit=10):
    sem = asyncio.Semaphore(limit)

    async def fetch_one(url):
        async with sem:
            return await http_get(url)

    return await asyncio.gather(*(fetch_one(u) for u in urls))

The trap to name: calling a blocking function like time.sleep or a synchronous HTTP client inside a coroutine freezes every task on the loop. Use asyncio.to_thread for blocking calls. Go takes a third path with goroutines and channels; see our Go interview questions if that is your stack.

Common Concurrency Interview Pitfalls

These are the mistakes interviewers watch for, roughly in order of how often they cost candidates the round.

  1. if instead of while around wait. Spurious wakeups and stolen wakeups both break if.
  2. Unlocking outside finally. An exception leaves the lock held forever. In Python, use with.
  3. Using notify with a shared wait set. With one monitor and two kinds of waiters, notify can wake the wrong thread. Use notifyAll or separate conditions.
  4. Holding a lock during slow I/O. It serializes your threads and widens deadlock windows. Copy what you need, release, then do I/O.
  5. Synchronizing with sleep. "Sleep 100 ms and hope the other thread finished" is not synchronization. Use a latch, event, or join.
  6. Swallowing InterruptedException. Restore the flag with Thread.currentThread().interrupt() or propagate it.
  7. Assuming the GIL or volatile gives atomicity. Neither protects read-modify-write sequences.
  8. Check-then-act on concurrent collections. Use atomic methods like computeIfAbsent.
  9. Inconsistent lock order. Pick an order, state it out loud, and follow it everywhere.

Saying these checks aloud matters as much as coding them. Walk the interviewer through one bad interleaving and show how your code prevents it; our guide on thinking out loud in coding interviews has phrasing that works.

How to Prepare for a Concurrency Round

Start with LeetCode's concurrency category: Print in Order (1114), Print FooBar Alternately (1115), Building H2O (1117), Design Bounded Blocking Queue (1188), and The Dining Philosophers (1226). Then write the four implementations above from memory in your interview language, once with library primitives and once from a lock and conditions.

Language matters here more than in algorithm rounds, because the primitives differ. Java has the richest standard toolkit in java.util.concurrent, which is why many backend loops default to it; our Java interview questions guide covers executors and the memory model in more depth. C++ candidates should know std::mutex, std::condition_variable, and std::atomic, covered in our C++ interview questions.

Expect concurrency to surface outside dedicated rounds too. Low-level design prompts like a parking lot design often end with "now make it thread-safe," and system design questions like a distributed cache scale the same ideas across machines. For where these rounds fit in a full loop, see the backend engineer interview guide.

Concurrency bugs hide in interleavings you cannot test in 45 minutes. TechScreen runs invisibly during your screen share and helps you check lock ordering, condition-variable logic, and thread-safety follow-ups as they come. Try it with 3 free tokens before your next backend round.

Get started free →

Frequently Asked Questions

What is the difference between a mutex and a semaphore?

A mutex is a lock with one owner: only the thread that acquired it should release it, and it allows exactly one thread into a critical section. A semaphore is a counter of permits that allows up to N threads in at once, and any thread can release a permit. Use a mutex to protect shared data and a semaphore to cap concurrent access to a limited resource, such as a pool of database connections or outbound API calls.

What are the four conditions for deadlock?

Deadlock needs all four Coffman conditions at once: mutual exclusion (a resource can be held by only one thread), hold and wait (a thread holds one resource while waiting for another), no preemption (resources cannot be forcibly taken away), and circular wait (a cycle of threads each waiting on the next). Breaking any one condition prevents deadlock. In practice, the most common fix is a global lock ordering, which removes circular wait.

Why should you use a while loop instead of an if statement when waiting on a condition variable?

A thread can wake from a condition variable wait without the condition being true. Spurious wakeups are allowed by both Java and POSIX, and another thread may also grab the state between the notify and your thread reacquiring the lock. Re-checking the predicate in a while loop guarantees the thread proceeds only when the condition actually holds. Using if is one of the most common bugs interviewers look for.

Does Python's GIL make code thread-safe?

No. The global interpreter lock prevents two threads from executing Python bytecode at the same moment, but a statement like counter += 1 compiles to several bytecode steps, and a thread switch can happen between them. You still need a Lock for compound operations. The GIL also limits CPU-bound parallelism in standard CPython, which is why CPU-heavy work often uses multiprocessing, though free-threaded CPython builds are changing this.

When should I use async instead of threads?

Use async when the workload is many concurrent I/O-bound tasks, such as thousands of network calls or open sockets, because coroutines are far cheaper than OS threads and switch only at explicit await points. Use threads when you call blocking libraries that are not async-aware, or when you need parallelism on multiple cores in languages that support it. For CPU-bound work in Python, use processes.

Which LeetCode problems cover concurrency?

LeetCode has a dedicated concurrency category. The most useful problems are Print in Order (1114), Print FooBar Alternately (1115), Print Zero Even Odd (1116), Building H2O (1117), Design Bounded Blocking Queue (1188), Fizz Buzz Multithreaded (1195), The Dining Philosophers (1226), and Web Crawler Multithreaded (1242). Some are premium. Solve them in both Java and Python to learn each language's primitives.

What is the difference between a process and a thread?

A process is an independent program instance with its own memory address space, file handles, and resources, isolated from other processes by the operating system. A thread is a unit of execution inside a process that shares the process's memory with its sibling threads. Threads are cheaper to create and communicate through shared memory, which makes them faster but exposes them to race conditions. Processes communicate through pipes, sockets, or shared memory segments.

Ready to use AI assistance in your next interview?

TechScreen is the invisible AI assistant trusted by engineers interviewing at Google, Meta, Amazon, and hundreds of other companies. Start with 3 free tokens — no credit card required.

Ace your next interview →