# Concurrency and Multi-Threading Interview Questions (2025)

Source: https://www.techinterview.org/post/3233460494/concurrency-multithreading-interview-questions/
Updated: 2026-04-16 · techinterview.org

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)

- **Mutual exclusion**: a resource can only be held by one thread

- **Hold and wait**: a thread holds a resource while waiting for another

- **No preemption**: resources cannot be forcibly taken from a thread

- **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.
