Concurrency and Multi-Threading Interview Questions (2025)

Concurrency questions appear at almost every senior engineering interview. Interviewers test whether you can reason about race conditions, design thread-safe data structures, and choose the right synchronization primitive. This guide covers the concepts, the problems, and the Java/Python/Go implementations you need.

Processes vs Threads vs Coroutines

Concept Memory Switching Cost Communication
Process Isolated address space High (full context switch, TLB flush) IPC (pipes, sockets, shared memory)
Thread Shared within process Medium (save/restore registers) Shared memory (requires synchronization)
Coroutine Shared within thread Very low (cooperative yield, user-space) Shared memory (often single-threaded, no locks needed)

Race Conditions

# Classic race condition: unsynchronized counter
import threading

counter = 0

def increment():
    global counter
    for _ in range(1_000_000):
        counter += 1  # NOT atomic: read, add, write — 3 operations

threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()

print(counter)  # Should be 5,000,000 but often much less — race condition!

# Fix 1: threading.Lock
lock = threading.Lock()
def increment_safe():
    global counter
    for _ in range(1_000_000):
        with lock:
            counter += 1

# Fix 2: threading.local() for per-thread state, combine at end
# Fix 3: multiprocessing.Value with lock for shared integer

Deadlock

Four Conditions for Deadlock (Coffman Conditions)

  1. Mutual exclusion: a resource can only be held by one thread
  2. Hold and wait: a thread holds a resource while waiting for another
  3. No preemption: resources cannot be forcibly taken from a thread
  4. Circular wait: Thread A waits for B, B waits for A (cycle)
// Classic deadlock: two threads locking two locks in opposite order
Object lockA = new Object();
Object lockB = new Object();

// Thread 1
synchronized (lockA) {
    Thread.sleep(10);       // simulate work
    synchronized (lockB) {  // waits for lockB — which Thread 2 holds
        // ...
    }
}

// Thread 2 (runs concurrently)
synchronized (lockB) {
    Thread.sleep(10);
    synchronized (lockA) {  // waits for lockA — which Thread 1 holds
        // ...              // DEADLOCK
    }
}

// Fix: always acquire locks in the same order
// Thread 1 and Thread 2 both lock A before B — no circular wait

Deadlock Prevention Strategies

  • Lock ordering: globally order all locks; always acquire in that order
  • Lock timeout: use tryLock(timeout) — if timeout expires, back off and retry
  • Lock-free data structures: use CAS (compare-and-swap) instead of locks
  • Single-threaded event loop: no shared state, no locks needed (Node.js, Redis)

Java Concurrency Utilities

Synchronized vs ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

// synchronized: simple, held until method/block exits
synchronized (this) {
    sharedList.add(item);
}

// ReentrantLock: more control
ReentrantLock lock = new ReentrantLock();

// Try-lock with timeout
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    try {
        sharedList.add(item);
    } finally {
        lock.unlock();  // ALWAYS unlock in finally
    }
} else {
    // Could not acquire lock — handle gracefully
}

// ReentrantReadWriteLock for read-heavy workloads
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();    // multiple readers simultaneously
rwLock.writeLock().lock();   // exclusive write

Atomic Variables

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();          // thread-safe increment
counter.compareAndSet(expected, newValue);  // CAS operation

// Compare-and-set: only updates if current value == expected
// This is the foundation of all lock-free algorithms

// AtomicReference for lock-free linked list / stack
class LockFreeStack<T> {
    private AtomicReference<Node<T>> top = new AtomicReference<>(null);

    public void push(T val) {
        Node<T> newNode = new Node<>(val);
        Node<T> current;
        do {
            current = top.get();
            newNode.next = current;
        } while (!top.compareAndSet(current, newNode));  // retry if stale
    }
}

ExecutorService and Thread Pools

import java.util.concurrent.*;

// Fixed thread pool: bounded — controls max parallelism
ExecutorService pool = Executors.newFixedThreadPool(8);

// Submit tasks
Future<Integer> future = pool.submit(() -> {
    return computeExpensiveThing();
});

// Block until result is ready
int result = future.get();       // throws ExecutionException if task threw
int result2 = future.get(5, TimeUnit.SECONDS);  // timeout version

// CompletableFuture: composable async (Java 8+)
CompletableFuture.supplyAsync(() -> fetchUser(id), pool)
    .thenApply(user -> enrichUser(user))
    .thenAccept(enrichedUser -> sendEmail(enrichedUser))
    .exceptionally(ex -> { logError(ex); return null; });

// Always shut down the pool
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);

BlockingQueue — Producer-Consumer Pattern

BlockingQueue<Task> queue = new LinkedBlockingQueue<>(1000);

// Producer
void produce() throws InterruptedException {
    queue.put(generateTask());   // blocks if queue is full
}

// Consumer
void consume() throws InterruptedException {
    while (true) {
        Task task = queue.take();  // blocks if queue is empty
        process(task);
    }
}

// This pattern naturally handles backpressure:
// producers slow down when consumers are overwhelmed

Python Concurrency: threading vs multiprocessing vs asyncio

Mechanism True Parallelism? Use For
threading No (GIL) I/O-bound: network calls, file I/O
multiprocessing Yes CPU-bound: image processing, ML inference, crypto
asyncio No (single thread) High-concurrency I/O: thousands of network connections
import asyncio
import aiohttp

async def fetch(session, url):
    async with session.get(url) as resp:
        return await resp.text()

async def main():
    urls = ["https://httpbin.org/get"] * 100

    async with aiohttp.ClientSession() as session:
        # Run all 100 requests concurrently (but on one thread!)
        results = await asyncio.gather(*[fetch(session, u) for u in urls])

    return results

# asyncio is perfect for N concurrent I/O operations
# without spawning N threads or processes
asyncio.run(main())

Go Concurrency: Goroutines and Channels

package main

import (
    "fmt"
    "sync"
)

// Goroutine: lightweight user-space thread (starts at 2KB stack)
// Go runtime multiplexes goroutines onto OS threads (M:N threading)

func producer(ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 10; i++ {
        ch <- i  // send to channel (blocks if channel is full)
    }
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for val := range ch {  // receive until channel is closed
        fmt.Println("consumed:", val)
    }
}

func main() {
    ch := make(chan int, 5)  // buffered channel: producer does not block until full

    var wg sync.WaitGroup
    wg.Add(2)
    go producer(ch, &wg)
    go consumer(ch, &wg)

    // Close channel after producer is done so consumer can exit range loop
    go func() {
        wg.Wait()
    }()
}

// select: wait on multiple channels (like a switch for channels)
select {
case msg := <-ch1:
    handle(msg)
case msg := <-ch2:
    handle(msg)
case <-time.After(5 * time.Second):
    fmt.Println("timeout")
}

Common Interview Problems

Implement a Thread-Safe LRU Cache

import java.util.*;
import java.util.concurrent.locks.*;

class ThreadSafeLRU<K, V> {
    private final int capacity;
    private final LinkedHashMap<K, V> cache;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public ThreadSafeLRU(int capacity) {
        this.capacity = capacity;
        this.cache = new LinkedHashMap<>(capacity, 0.75f, true) {
            protected boolean removeEldestEntry(Map.Entry<K, V> e) {
                return size() > capacity;
            }
        };
    }

    public V get(K key) {
        lock.readLock().lock();
        try { return cache.getOrDefault(key, null); }
        finally { lock.readLock().unlock(); }
    }

    public void put(K key, V value) {
        lock.writeLock().lock();
        try { cache.put(key, value); }
        finally { lock.writeLock().unlock(); }
    }
}

Implement a Semaphore

class BoundedSemaphore:
    def __init__(self, max_count: int):
        self._count = 0
        self._max = max_count
        self._lock = threading.Lock()
        self._condition = threading.Condition(self._lock)

    def acquire(self):
        with self._condition:
            while self._count >= self._max:
                self._condition.wait()  # releases lock and blocks
            self._count += 1

    def release(self):
        with self._condition:
            self._count -= 1
            self._condition.notify()    # wake one waiting thread

Frequently Asked Questions

What is a race condition and how do you prevent it?

A race condition occurs when the outcome of a program depends on the timing of uncontrolled events — specifically, when multiple threads access shared mutable state without synchronization, and at least one access is a write. The classic example is a counter incremented by multiple threads: reading the value, adding one, and writing back are three separate operations that can interleave, causing lost updates. Prevention: use atomic operations (AtomicInteger in Java, atomic in C++), mutual exclusion locks (synchronized, threading.Lock), or design for immutability and message passing (channels in Go, actors) to eliminate shared mutable state entirely.

What are the four conditions for deadlock?

Deadlock requires all four Coffman conditions to hold simultaneously: (1) Mutual exclusion — at least one resource can only be held by one thread at a time. (2) Hold and wait — a thread holds a resource while waiting to acquire another. (3) No preemption — resources cannot be forcibly taken from a thread; it must release voluntarily. (4) Circular wait — there exists a cycle of threads, each waiting for a resource held by the next. Remove any one condition to prevent deadlock: use lock ordering (eliminates circular wait), use tryLock with timeout (eliminates indefinite hold-and-wait), or use lock-free data structures (eliminates mutual exclusion requirement).

When should you use threading vs multiprocessing vs asyncio in Python?

Python threading is best for I/O-bound tasks (network requests, file I/O, database calls) — even though the GIL prevents true CPU parallelism, it is released during I/O waits, allowing threads to overlap waiting time. Multiprocessing bypasses the GIL by using separate processes with separate memory — use it for CPU-bound work (image processing, numerical computation, ML inference). asyncio is best for very high concurrency I/O scenarios (thousands of simultaneous connections) — it uses a single thread with cooperative scheduling (coroutines yield control), avoiding the overhead of threads while handling massive concurrency. Rule of thumb: asyncio > threading for I/O if you can use async libraries; multiprocessing for CPU work.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is a race condition and how do you prevent it?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A race condition occurs when the outcome of a program depends on the timing of uncontrolled events — specifically, when multiple threads access shared mutable state without synchronization, and at least one access is a write. The classic example is a counter incremented by multiple threads: reading the value, adding one, and writing back are three separate operations that can interleave, causing lost updates. Prevention: use atomic operations (AtomicInteger in Java, atomic in C++), mutual exclusion locks (synchronized, threading.Lock), or design for immutability and message passing (channels in Go, actors) to eliminate shared mutable state entirely.”
}
},
{
“@type”: “Question”,
“name”: “What are the four conditions for deadlock?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Deadlock requires all four Coffman conditions to hold simultaneously: (1) Mutual exclusion — at least one resource can only be held by one thread at a time. (2) Hold and wait — a thread holds a resource while waiting to acquire another. (3) No preemption — resources cannot be forcibly taken from a thread; it must release voluntarily. (4) Circular wait — there exists a cycle of threads, each waiting for a resource held by the next. Remove any one condition to prevent deadlock: use lock ordering (eliminates circular wait), use tryLock with timeout (eliminates indefinite hold-and-wait), or use lock-free data structures (eliminates mutual exclusion requirement).”
}
},
{
“@type”: “Question”,
“name”: “When should you use threading vs multiprocessing vs asyncio in Python?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Python threading is best for I/O-bound tasks (network requests, file I/O, database calls) — even though the GIL prevents true CPU parallelism, it is released during I/O waits, allowing threads to overlap waiting time. Multiprocessing bypasses the GIL by using separate processes with separate memory — use it for CPU-bound work (image processing, numerical computation, ML inference). asyncio is best for very high concurrency I/O scenarios (thousands of simultaneous connections) — it uses a single thread with cooperative scheduling (coroutines yield control), avoiding the overhead of threads while handling massive concurrency. Rule of thumb: asyncio > threading for I/O if you can use async libraries; multiprocessing for CPU work.”
}
}
]
}

Scroll to Top