Concurrency and multithreading questions appear in almost every senior backend, Java, Go, and systems programming interview. They test whether you understand race conditions, deadlocks, memory visibility, and how to design thread-safe code. This guide covers the 50 most common questions with concise answers and code examples across Java, Go, and Python.
Quick reference
| Topic | Most asked questions |
|---|---|
| Fundamentals | Thread vs process, concurrency vs parallelism |
| Java threads | Thread lifecycle, synchronized, volatile, wait/notify |
| Locks & Synchronization | Mutex, semaphore, ReentrantLock, deadlock |
| Java Memory Model | happens-before, visibility, atomics |
| Executor Framework | ThreadPoolExecutor, Future, CompletableFuture |
| Concurrent Collections | ConcurrentHashMap, BlockingQueue, CopyOnWriteArrayList |
| Go concurrency | Goroutines, channels, select, WaitGroup, Mutex |
| Python | GIL, threading, multiprocessing, asyncio |
| Patterns | Producer-consumer, reader-writer, dining philosophers |
| Advanced | Fork-join, reactive streams, structured concurrency |
Fundamentals
1. What is the difference between concurrency and parallelism?
Concurrency is about dealing with multiple things at once (structure). Parallelism is about doing multiple things at once (execution).
| Aspect | Concurrency | Parallelism |
|---|---|---|
| Definition | Multiple tasks in progress simultaneously | Multiple tasks executing at the same instant |
| Requires multiple CPUs? | No (can interleave on 1 CPU) | Yes |
| Goal | Responsiveness, structure | Throughput, speed |
| Example | Web server handling 1000 requests on 4 cores | Matrix multiplication on GPU |
| Rob Pike quote | "Concurrency is about structure" | "Parallelism is about execution" |
Concurrency enables parallelism but doesn't require it. A single-core CPU can be concurrent (via context switching) but not parallel.
2. What is the difference between a thread and a process?
| Aspect | Process | Thread |
|---|---|---|
| Memory space | Isolated (own heap, stack, code) | Shared heap, own stack |
| Communication | IPC (pipes, sockets, shared memory) | Shared memory (but needs sync) |
| Creation cost | High (fork/exec) | Low |
| Crash isolation | One crash doesn't affect others | One thread crash can kill the process |
| Context switch cost | Expensive | Cheaper |
| Examples | Two Chrome windows | Two tabs in the same Chrome window |
3. What is a race condition?
A race condition occurs when the correctness of a program depends on the relative timing or interleaving of multiple threads.
// Race condition — counter may not reach 10000
public class Counter {
private int count = 0;
public void increment() {
count++; // read-modify-write: NOT atomic
}
}
// Fix 1: synchronized
public synchronized void increment() {
count++;
}
// Fix 2: AtomicInteger
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
The count++ operation is actually three steps: read, increment, write. Two threads can both read the old value, both increment, and both write the same result — losing one increment.
4. What is a deadlock and what are its four necessary conditions?
A deadlock is a state where two or more threads are permanently blocked, each waiting for a resource held by the other.
Four necessary conditions (Coffman conditions) — ALL must hold:
| Condition | Description |
|---|---|
| Mutual Exclusion | Resources cannot be shared; only one thread at a time |
| Hold and Wait | Thread holds ≥1 resource while waiting for others |
| No Preemption | Resources cannot be forcibly taken; only voluntarily released |
| Circular Wait | Thread A waits for B, B waits for C, C waits for A |
// Classic deadlock
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) { /* work */ } // acquires A then B
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
synchronized (lockA) { /* work */ } // acquires B then A
}
});
Prevention: always acquire locks in the same order, use tryLock() with timeout, or use lock-free data structures.
5. What is livelock? How does it differ from deadlock?
| Aspect | Deadlock | Livelock |
|---|---|---|
| Thread state | Blocked (waiting) | Active (running) but making no progress |
| Detection | Easier (threads stuck) | Harder (threads look busy) |
| Example | Two threads each waiting for the other's lock | Two people stepping aside in the same direction |
| Resolution | Break Coffman condition | Add randomness or backoff |
// Livelock — threads keep responding to each other but make no progress
while (true) {
if (otherThread.isWorking()) {
Thread.sleep(random.nextInt(100)); // polite backoff
continue;
}
doWork();
break;
}
6. What is starvation?
Starvation occurs when a thread is perpetually denied access to a resource because other threads are always given priority.
Common causes:
- Thread priority settings (low-priority thread never scheduled)
- Unfair locks (threads always queue-jump)
- Long-running threads monopolizing locks
Fix: Use fair locks (new ReentrantLock(true) in Java), avoid priority inversion, cap lock hold times.
Java Threads
7. What is the Java Thread lifecycle?
NEW → RUNNABLE → (RUNNING) → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
| State | Description | How to enter |
|---|---|---|
| NEW | Created but not started | new Thread(...) |
| RUNNABLE | Ready to run or running | thread.start() |
| BLOCKED | Waiting for monitor lock | synchronized block/method |
| WAITING | Waiting indefinitely | wait(), join(), LockSupport.park() |
| TIMED_WAITING | Waiting with timeout | sleep(ms), wait(ms), join(ms) |
| TERMINATED | Finished execution | run() returned or exception |
8. What are the ways to create a thread in Java?
// 1. Extend Thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running: " + Thread.currentThread().getName());
}
}
new MyThread().start();
// 2. Implement Runnable (preferred — no single inheritance wasted)
Thread t = new Thread(() -> System.out.println("Runnable lambda"));
t.start();
// 3. Implement Callable (returns a result)
Callable<Integer> callable = () -> 42;
FutureTask<Integer> ft = new FutureTask<>(callable);
new Thread(ft).start();
System.out.println(ft.get()); // 42
// 4. Executor framework (recommended for production)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("From pool"));
executor.shutdown();
Prefer Runnable/Callable over extending Thread — it separates task from execution mechanism.
9. What is the volatile keyword in Java?
volatile ensures that reads and writes to a variable go directly to main memory — not a thread's CPU cache. This provides visibility but NOT atomicity.
// Without volatile: thread B may never see thread A's update
private boolean running = true;
// With volatile: guaranteed visibility across threads
private volatile boolean running = true;
// Correct shutdown pattern
public void stop() { running = false; }
public void run() { while (running) { doWork(); } }
| Property | volatile |
synchronized |
AtomicInteger |
|---|---|---|---|
| Visibility | ✅ | ✅ | ✅ |
| Atomicity | ❌ (except long/double) | ✅ | ✅ |
| Mutual exclusion | ❌ | ✅ | ❌ (CAS) |
| Performance | Fast | Slower | Fast |
10. What is the synchronized keyword? When do you use it?
synchronized acquires the intrinsic lock (monitor) of an object before executing the block. Only one thread can hold the lock at a time.
public class BankAccount {
private int balance = 1000;
// Synchronized method — lock on 'this'
public synchronized void deposit(int amount) {
balance += amount;
}
// Synchronized block — lock on specific object (more granular)
private final Object lock = new Object();
public void withdraw(int amount) {
synchronized (lock) {
if (balance >= amount) balance -= amount;
}
}
// Static synchronized — lock on Class object
public static synchronized void classLevelOp() { /* ... */ }
}
Use synchronized when: you need mutual exclusion + visibility. For read-heavy scenarios, prefer ReadWriteLock.
11. What is the difference between wait() and sleep()?
| Aspect | wait() |
sleep() |
|---|---|---|
| Defined in | Object |
Thread |
| Releases lock? | ✅ Yes | ❌ No |
Called inside synchronized? |
Must be | Optional |
| Woken by | notify() / notifyAll() / timeout |
Timeout |
| Used for | Inter-thread communication | Pausing execution |
| InterruptedException | Throws | Throws |
// wait/notify producer-consumer
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // releases lock, waits
}
process(queue.poll());
}
// producer
synchronized (queue) {
queue.add(item);
queue.notifyAll(); // wake waiting consumers
}
12. What is notify() vs notifyAll()?
| Method | Wakes | When to use |
|---|---|---|
notify() |
One random waiting thread | Only if all waiters are interchangeable |
notifyAll() |
All waiting threads | Multiple different conditions; safer default |
Prefer notifyAll() to avoid missed signals when multiple threads wait on different conditions.
13. What is the Java Memory Model (JMM)?
The JMM defines rules for when writes by one thread become visible to another.
happens-before relationship: If action A happens-before B, then A's effects are visible to B.
Key happens-before rules:
- Monitor lock: unlock happens-before subsequent lock of the same monitor
- volatile write: write to volatile happens-before subsequent read
- Thread start:
t.start()happens-before any action in threadt - Thread join: all actions in thread
thappen-beforet.join()returns - Transitivity: if A hb B and B hb C → A hb C
Without happens-before, you can get stale values, out-of-order execution, and visibility bugs.
Locks & Synchronization
14. What is ReentrantLock? How does it differ from synchronized?
import java.util.concurrent.locks.ReentrantLock;
private final ReentrantLock lock = new ReentrantLock();
public void transfer(int amount) {
lock.lock();
try {
// critical section
balance -= amount;
} finally {
lock.unlock(); // ALWAYS unlock in finally
}
}
| Feature | synchronized |
ReentrantLock |
|---|---|---|
| Interruptible lock wait | ❌ | ✅ lockInterruptibly() |
| Timed try-lock | ❌ | ✅ tryLock(timeout) |
| Fairness option | ❌ (unfair) | ✅ new ReentrantLock(true) |
| Multiple conditions | ❌ (one wait set) | ✅ newCondition() |
| Non-block try | ❌ | ✅ tryLock() |
| Auto-release | ✅ (implicit) | ❌ (must call unlock()) |
15. What is a ReadWriteLock?
ReadWriteLock allows multiple concurrent readers but only one writer at a time (exclusive write, shared read).
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private Map<String, String> cache = new HashMap<>();
public String get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, String value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
Best for: read-heavy, write-rare workloads (e.g., in-memory caches, config stores).
16. What is a mutex vs a semaphore?
| Aspect | Mutex | Semaphore |
|---|---|---|
| Ownership | Thread that locked must unlock | Any thread can release |
| Count | Binary (0 or 1) | Integer (0 to N) |
| Use case | Mutual exclusion | Rate limiting, resource pools |
| Java | ReentrantLock |
java.util.concurrent.Semaphore |
// Semaphore — allow max 3 concurrent DB connections
Semaphore semaphore = new Semaphore(3);
public void queryDB() throws InterruptedException {
semaphore.acquire(); // blocks if 3 already acquired
try {
db.query("...");
} finally {
semaphore.release();
}
}
17. What is a StampedLock?
StampedLock (Java 8+) adds optimistic reading to avoid locking for short reads.
StampedLock sl = new StampedLock();
// Optimistic read — no lock acquired
long stamp = sl.tryOptimisticRead();
double x = this.x;
double y = this.y;
if (!sl.validate(stamp)) { // check if write happened
stamp = sl.readLock();
try { x = this.x; y = this.y; }
finally { sl.unlockRead(stamp); }
}
Faster than ReadWriteLock for read-heavy workloads; not reentrant.
Executor Framework
18. What is the Executor framework? What thread pool types exist?
The Executor framework decouples task submission from execution.
| Factory method | Description | Use case |
|---|---|---|
newFixedThreadPool(n) |
n threads, unbounded queue | CPU-bound tasks |
newCachedThreadPool() |
Unbounded threads, 60s idle timeout | Short-lived async tasks |
newSingleThreadExecutor() |
1 thread, tasks queued | Sequential background work |
newScheduledThreadPool(n) |
Timed/recurring tasks | Cron-like scheduling |
newWorkStealingPool() |
ForkJoinPool with n CPUs | Recursive parallelism |
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> heavyComputation());
Integer result = future.get(5, TimeUnit.SECONDS); // block with timeout
pool.shutdown();
pool.awaitTermination(60, TimeUnit.SECONDS);
19. What is ThreadPoolExecutor and its constructor parameters?
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // corePoolSize: always-alive threads
8, // maximumPoolSize: max threads when queue full
60, TimeUnit.SECONDS, // keepAliveTime: idle thread timeout
new ArrayBlockingQueue<>(100), // workQueue: task buffer
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
);
Rejection policies:
| Policy | Behaviour |
|---|---|
AbortPolicy (default) |
Throw RejectedExecutionException |
CallerRunsPolicy |
Caller thread runs the task (back-pressure) |
DiscardPolicy |
Silently discard |
DiscardOldestPolicy |
Drop oldest queued task |
20. What is CompletableFuture? How does it improve on Future?
// Old Future — blocks the calling thread
Future<String> future = executor.submit(() -> fetchData());
String result = future.get(); // BLOCKS
// CompletableFuture — non-blocking chaining
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> fetchUser(userId)) // async
.thenApply(user -> user.getName()) // transform
.thenCombine(fetchAddress(userId), (name, addr) -> name + " " + addr)
.exceptionally(ex -> "default") // error fallback
.whenComplete((result, ex) -> log(result)); // always runs
| Feature | Future |
CompletableFuture |
|---|---|---|
| Non-blocking chaining | ❌ | ✅ |
| Manual completion | ❌ | ✅ complete(value) |
| Exception handling | ❌ | ✅ exceptionally() |
| Combining futures | ❌ | ✅ thenCombine(), allOf() |
Java Atomic & Concurrent Collections
21. What are Java atomic classes?
Atomic classes use Compare-And-Swap (CAS) hardware instructions for lock-free thread-safety.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic increment
counter.compareAndSet(5, 10); // only sets to 10 if current == 5
AtomicReference<String> ref = new AtomicReference<>("hello");
ref.compareAndSet("hello", "world");
// LongAdder — better throughput than AtomicLong under high contention
LongAdder adder = new LongAdder();
adder.increment();
long total = adder.sum();
| Class | Use case |
|---|---|
AtomicInteger/Long/Boolean |
Single counter |
AtomicReference<T> |
Single object reference |
AtomicReferenceArray<T> |
Array of references |
LongAdder / LongAccumulator |
High-contention counters |
22. What is ConcurrentHashMap? How does it work?
ConcurrentHashMap divides the map into segments (Java 7) or uses per-bucket CAS+synchronized (Java 8+), allowing concurrent reads without locking and concurrent writes to different buckets.
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.get("key"); // no lock required
// Atomic compute operations
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
map.computeIfAbsent("key", k -> loadFromDB(k));
map.merge("key", 1, Integer::sum);
| Operation | Thread-safe? | Notes |
|---|---|---|
get() |
✅ | Lock-free |
put() |
✅ | Per-bucket sync |
putIfAbsent() |
✅ | Atomic |
| Iteration | ✅ | Weakly consistent |
size() |
✅ (approx) | May not reflect concurrent mutations |
23. What concurrent collection types are in java.util.concurrent?
| Collection | Description | Use case |
|---|---|---|
ConcurrentHashMap |
Thread-safe hash map | General concurrent map |
CopyOnWriteArrayList |
Write creates new copy | Read-heavy, rare writes |
CopyOnWriteArraySet |
Set backed by COW list | Small sets, read-heavy |
LinkedBlockingQueue |
Bounded/unbounded queue | Producer-consumer |
ArrayBlockingQueue |
Bounded array queue | Fixed-capacity queues |
PriorityBlockingQueue |
Priority-ordered queue | Task scheduling |
SynchronousQueue |
Zero-capacity handoff | Direct thread-to-thread |
ConcurrentLinkedQueue |
Non-blocking linked list | High-throughput queues |
DelayQueue |
Elements with delay | Scheduled task queues |
24. What is ThreadLocal?
ThreadLocal provides per-thread variable storage — each thread sees its own isolated copy.
// Database connection per thread
ThreadLocal<Connection> conn = ThreadLocal.withInitial(() -> dataSource.getConnection());
// UserContext per HTTP request (common in web frameworks)
ThreadLocal<User> currentUser = new ThreadLocal<>();
public void handleRequest(User user) {
currentUser.set(user);
try {
service.process(); // can call currentUser.get() anywhere in call stack
} finally {
currentUser.remove(); // CRITICAL: prevent memory leaks in thread pools
}
}
Warning: Always call remove() in thread pools — threads are reused, and stale values leak between requests.
Go Concurrency
25. What are goroutines? How are they different from threads?
// Start a goroutine — extremely lightweight (2KB initial stack)
go func() {
fmt.Println("I'm a goroutine")
}()
// Goroutines are multiplexed onto OS threads by the Go runtime (M:N model)
| Aspect | OS Thread | Goroutine |
|---|---|---|
| Initial stack | ~1MB | ~2KB (grows dynamically) |
| Managed by | OS | Go runtime scheduler |
| Context switch | ~1µs (kernel) | ~100ns (user-space) |
| Max count | ~thousands | ~millions |
| Communication | Shared memory | Channels (prefer) |
26. What are channels in Go?
Channels are typed conduits for passing data between goroutines.
// Unbuffered channel — sender blocks until receiver is ready
ch := make(chan int)
go func() { ch <- 42 }()
val := <-ch // 42
// Buffered channel — sender blocks only when buffer is full
buffered := make(chan string, 3)
buffered <- "a"
buffered <- "b"
buffered <- "c"
// buffered <- "d" // blocks: buffer full
// Directional types
func producer(out chan<- int) { out <- 1 }
func consumer(in <-chan int) { fmt.Println(<-in) }
// Range over channel until closed
go func() {
for i := 0; i < 5; i++ { ch <- i }
close(ch)
}()
for val := range ch { fmt.Println(val) }
27. What is select in Go?
select lets a goroutine wait on multiple channel operations, choosing whichever is ready first.
select {
case msg := <-ch1:
fmt.Println("received from ch1:", msg)
case msg := <-ch2:
fmt.Println("received from ch2:", msg)
case ch3 <- "hello":
fmt.Println("sent to ch3")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("non-blocking: nothing ready")
}
28. How do you synchronize goroutines with WaitGroup and Mutex?
import "sync"
// WaitGroup — wait for multiple goroutines to finish
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
doWork(id)
}(i)
}
wg.Wait()
// Mutex — mutual exclusion
var mu sync.Mutex
counter := 0
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
// RWMutex — for read-heavy workloads
var rwmu sync.RWMutex
rwmu.RLock() // multiple readers OK
data := cache[key]
rwmu.RUnlock()
rwmu.Lock() // exclusive write
cache[key] = val
rwmu.Unlock()
29. What is the Go race detector?
go run -race main.go # detect races at runtime
go test -race ./... # run tests with race detector
The race detector instruments memory accesses and reports when two goroutines concurrently access the same variable without synchronization (and at least one is a write).
Best practice: always run go test -race in CI.
Python Concurrency
30. What is Python's GIL (Global Interpreter Lock)?
The GIL is a mutex in CPython that allows only one thread to execute Python bytecode at a time.
| Scenario | Threads | Multiprocessing |
|---|---|---|
| CPU-bound (computation) | ❌ No speedup (GIL blocks) | ✅ Full parallelism |
| I/O-bound (network, disk) | ✅ Works well (GIL released during I/O) | ✅ Also works |
| Memory overhead | Low | High (separate process) |
# CPU-bound: use multiprocessing, not threading
from multiprocessing import Pool
def square(n): return n * n
with Pool(4) as p:
results = p.map(square, range(10000))
# I/O-bound: threading works fine
import threading, requests
def fetch(url):
resp = requests.get(url)
print(f"{url}: {resp.status_code}")
threads = [threading.Thread(target=fetch, args=(url,)) for url in urls]
[t.start() for t in threads]
[t.join() for t in threads]
Note: Python 3.13 introduces an experimental free-threaded mode (--disable-gil) that removes the GIL.
31. What is asyncio in Python?
asyncio implements cooperative concurrency — a single thread, event-loop based. Coroutines voluntarily yield control at await points.
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)
return results
asyncio.run(main())
| Model | Mechanism | Best for |
|---|---|---|
threading |
OS threads (GIL limited for CPU) | I/O-bound, simple |
multiprocessing |
Separate processes | CPU-bound |
asyncio |
Event loop, single thread | High-concurrency I/O |
concurrent.futures |
Unified API over threads/processes | Both |
Concurrency Patterns
32. What is the Producer-Consumer pattern?
// Java with BlockingQueue
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);
// Producer
Runnable producer = () -> {
while (running) {
Task task = createTask();
queue.put(task); // blocks if full
}
};
// Consumer
Runnable consumer = () -> {
while (running || !queue.isEmpty()) {
Task task = queue.poll(100, TimeUnit.MILLISECONDS);
if (task != null) process(task);
}
};
// Go with buffered channel
jobs := make(chan Job, 100)
// Producer goroutine
go func() {
for _, job := range allJobs {
jobs <- job
}
close(jobs)
}()
// Consumer goroutines
for w := 0; w < 4; w++ {
go func() {
for job := range jobs {
process(job)
}
}()
}
33. What is the Reader-Writer problem?
Multiple readers can read simultaneously, but a writer needs exclusive access.
ReadWriteLock rwLock = new ReentrantReadWriteLock(true); // fair
// Allows N concurrent readers
public Data read() {
rwLock.readLock().lock();
try { return data; }
finally { rwLock.readLock().unlock(); }
}
// Exclusive write
public void write(Data d) {
rwLock.writeLock().lock();
try { data = d; }
finally { rwLock.writeLock().unlock(); }
}
34. What is the Fork-Join framework?
Fork-Join uses work-stealing for recursive parallelism — idle threads steal tasks from busy threads' queues.
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
class MergeSort extends RecursiveTask<int[]> {
private final int[] arr;
@Override
protected int[] compute() {
if (arr.length <= 1) return arr;
int mid = arr.length / 2;
MergeSort left = new MergeSort(Arrays.copyOfRange(arr, 0, mid));
MergeSort right = new MergeSort(Arrays.copyOfRange(arr, mid, arr.length));
left.fork(); // submit left subtask async
int[] rightResult = right.compute(); // compute right inline
int[] leftResult = left.join(); // wait for left
return merge(leftResult, rightResult);
}
}
int[] sorted = ForkJoinPool.commonPool().invoke(new MergeSort(data));
35. What is the Double-Checked Locking pattern?
// BROKEN in Java without volatile (pre-JDK 5)
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 3-step: alloc, init, assign
}
}
}
return instance;
}
// CORRECT: volatile prevents instruction reordering
private static volatile Singleton instance;
// BETTER: Use initialization-on-demand holder (no volatile needed)
public class Singleton {
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() { return Holder.INSTANCE; }
}
Advanced Topics
36. What is CAS (Compare-And-Swap)?
CAS is a hardware-level atomic instruction: compareAndSwap(address, expected, newValue) — sets memory to newValue only if it currently equals expected.
// What AtomicInteger.incrementAndGet() does under the hood:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next)) // hardware CAS
return next;
// loop if another thread changed it (ABA problem possible)
}
}
ABA problem: Value changes A→B→A. CAS sees A (matches) and succeeds, even though an intermediate change occurred.
Fix: AtomicStampedReference — pairs value with a version stamp.
37. What is the ABA problem and how do you fix it?
// ABA problem scenario:
// Thread 1 reads value = "A"
// Thread 2 changes "A" → "B" → "A"
// Thread 1 CAS succeeds (sees "A") but state has changed
// Fix: AtomicStampedReference — stamp (version) increments on each change
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String val = ref.get(stampHolder);
int currentStamp = stampHolder[0];
// Only succeeds if BOTH value and stamp match
ref.compareAndSet("A", "B", currentStamp, currentStamp + 1);
38. What are CountDownLatch and CyclicBarrier?
// CountDownLatch — wait for N events (one-shot)
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
doWork(id);
latch.countDown(); // decrement
}).start();
}
latch.await(); // blocks until count reaches 0
System.out.println("All workers done");
// CyclicBarrier — all threads wait at a checkpoint, then continue (reusable)
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Phase complete"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
doPhase1();
barrier.await(); // wait for all threads to reach here
doPhase2();
barrier.await(); // barrier resets automatically
}).start();
}
CountDownLatch |
CyclicBarrier |
|
|---|---|---|
| Reusable | ❌ | ✅ |
| Who decrements | Any thread | Waiting threads |
| Barrier action | ❌ | ✅ (optional Runnable) |
39. What is Phaser?
Phaser is a flexible, reusable synchronisation barrier (generalises both CountDownLatch and CyclicBarrier).
Phaser phaser = new Phaser(3); // 3 registered parties
for (int t = 0; t < 3; t++) {
new Thread(() -> {
doPhaseA();
phaser.arriveAndAwaitAdvance(); // wait for all
doPhaseB();
phaser.arriveAndDeregister(); // done, deregister
}).start();
}
40. What is Exchanger?
Exchanger<V> lets two threads swap objects at a synchronisation point.
Exchanger<List<Integer>> exchanger = new Exchanger<>();
// Thread 1: produces data, receives consumer's empty buffer
Thread producer = new Thread(() -> {
List<Integer> buffer = fillBuffer();
List<Integer> emptyBuffer = exchanger.exchange(buffer);
// now has emptyBuffer to refill
});
// Thread 2: consumes full buffer, hands back empty
Thread consumer = new Thread(() -> {
List<Integer> fullBuffer = exchanger.exchange(new ArrayList<>());
consume(fullBuffer);
});
Performance & Design Questions
41. When should you use thread pools vs. virtual threads (Java 21+)?
// Traditional thread pool (OS threads)
ExecutorService pool = Executors.newFixedThreadPool(200);
// Virtual threads (Java 21) — lightweight, millions possible
ExecutorService vThreads = Executors.newVirtualThreadPerTaskExecutor();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> { Thread.sleep(1000); return null; });
}
}
| Aspect | Platform Threads | Virtual Threads |
|---|---|---|
| Memory per thread | ~1MB | ~few KB |
| Max practical count | ~thousands | ~millions |
| Blocking I/O | Blocks OS thread | Unmounts from carrier |
| CPU-bound | Good | No benefit |
synchronized blocks |
Safe | Risk of pinning (Java 21) |
Use virtual threads for: I/O-bound server tasks, high-concurrency request handling. Keep thread pools for: CPU-bound work, tasks needing thread affinity.
42. How do you avoid thread pool starvation?
Thread pool starvation occurs when all pool threads are blocked waiting for results from other tasks in the same pool.
// DEADLOCK risk: tasks submit subtasks to the same pool
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> {
Future<Integer> sub = pool.submit(() -> 42); // submits to full pool
return sub.get(); // blocks waiting — but pool is full!
});
// Fix 1: Use ForkJoinPool (work-stealing avoids starvation)
ForkJoinPool.commonPool().invoke(new RecursiveTask<>() { ... });
// Fix 2: Separate pools for different task types
ExecutorService ioPool = Executors.newFixedThreadPool(100);
ExecutorService cpuPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// Fix 3: CompletableFuture.supplyAsync with separate executor
CompletableFuture.supplyAsync(this::subtask, separateExecutor);
43. What is false sharing?
False sharing occurs when two unrelated variables share the same CPU cache line (typically 64 bytes). When one thread writes to variable A and another writes to variable B, both invalidate each other's cache — degrading performance.
// Bad: x and y likely on same cache line
class Counters {
volatile long x; // used by Thread 1
volatile long y; // used by Thread 2 — shares cache line with x!
}
// Fix: pad with unused fields to push to separate cache lines
class PaddedCounter {
volatile long x;
long p1, p2, p3, p4, p5, p6, p7; // 56 bytes padding
volatile long y;
}
// Java 8+: @Contended annotation (requires -XX:-RestrictContended)
@sun.misc.Contended
volatile long x;
44. What is lock striping?
Lock striping divides data into segments, each with its own lock — increasing concurrency without a global lock.
ConcurrentHashMap uses this: the map has 16+ buckets each with an independent lock, allowing 16+ concurrent writes.
// Custom lock-striped structure
private static final int STRIPES = 16;
private final Object[] locks = new Object[STRIPES];
private final Map<String, Integer>[] maps = new HashMap[STRIPES];
{ for (int i = 0; i < STRIPES; i++) { locks[i] = new Object(); maps[i] = new HashMap<>(); } }
public int get(String key) {
int idx = Math.abs(key.hashCode() % STRIPES);
synchronized (locks[idx]) { return maps[idx].get(key); }
}
45. What is structured concurrency (Java 21)?
Structured concurrency ensures that threads spawned within a scope are completed before the scope exits — making concurrent code easier to reason about.
import java.util.concurrent.StructuredTaskScope;
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<User> user = scope.fork(() -> fetchUser(id));
Supplier<Account> account = scope.fork(() -> fetchAccount(id));
scope.join(); // wait for both
scope.throwIfFailed(); // propagate first failure
return new Response(user.get(), account.get());
}
// When scope exits, all forked tasks are guaranteed done
Interview Scenarios
46. How would you implement a thread-safe singleton?
// Option 1: Enum (simplest, serialisation-safe, Bill Pugh's recommendation)
public enum Singleton {
INSTANCE;
public void doWork() { ... }
}
Singleton.INSTANCE.doWork();
// Option 2: Initialization-on-demand holder
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton(); // class loading is thread-safe
}
public static Singleton getInstance() { return Holder.INSTANCE; }
}
// Option 3: volatile double-checked locking (verbose but explicit)
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
47. How would you implement a thread-safe bounded blocking queue from scratch?
public class BoundedBlockingQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBlockingQueue(int capacity) { this.capacity = capacity; }
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) notFull.await();
queue.offer(item);
notEmpty.signal();
} finally { lock.unlock(); }
}
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) notEmpty.await();
T item = queue.poll();
notFull.signal();
return item;
} finally { lock.unlock(); }
}
}
48. How do you detect a deadlock in a running JVM?
# Get thread dump
jstack <pid>
# or
kill -3 <pid>
# Using JConsole / VisualVM — Threads tab → "Detect Deadlock"
# Programmatic detection
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] deadlocked = bean.findDeadlockedThreads();
if (deadlocked != null) {
ThreadInfo[] info = bean.getThreadInfo(deadlocked);
for (ThreadInfo ti : info) System.out.println(ti.toString());
}
Thread dump will show a cycle like:
Thread A waiting for lock owned by Thread B
Thread B waiting for lock owned by Thread A
49. What is reactive programming? How does it relate to concurrency?
Reactive programming models asynchronous data streams and propagation of change. It provides back-pressure — consumers control the rate at which producers emit.
// Project Reactor (used in Spring WebFlux)
Flux.range(1, 1000)
.filter(n -> n % 2 == 0)
.flatMap(n -> Mono.fromCallable(() -> heavyWork(n))
.subscribeOn(Schedulers.boundedElastic()))
.subscribe(result -> process(result));
| Aspect | Thread-based | Reactive |
|---|---|---|
| Model | Thread per connection | Event loop + callbacks |
| Concurrency | OS threads | Async pipelines |
| Back-pressure | Manual (queues) | Built-in |
| Error handling | try/catch | onError() |
| Frameworks | Spring MVC | Spring WebFlux, Vert.x |
50. What is the memory consistency effect of final fields in Java?
A correctly-constructed object's final fields are guaranteed to be visible to all threads without synchronisation, as long as the this reference doesn't escape the constructor.
class Config {
final String host; // guaranteed visible after construction
final int port; // same
volatile String mode; // needs volatile for post-construction writes
Config(String host, int port) {
this.host = host; // safe — final
this.port = port; // safe — final
// NEVER do: someGlobal = this; (this escapes before construction done)
}
}
This is why immutable objects are inherently thread-safe: final fields, set once in the constructor, are safely published to all threads.
Anti-patterns
| Anti-pattern | Problem | Fix |
|---|---|---|
Calling Thread.stop() |
Forcibly stops thread, may corrupt state | Use a volatile boolean flag |
Catching and ignoring InterruptedException |
Loses interrupt signal | Re-interrupt: Thread.currentThread().interrupt() |
Using HashMap in concurrent code |
Not thread-safe, data corruption | Use ConcurrentHashMap |
| Holding lock during I/O | Blocks all waiters | Release lock before I/O |
| Over-synchronizing | Unnecessary serialization, degrades perf | Use atomic variables or lock-free structures |
| Creating new threads per request | Thread creation overhead | Use thread pools |
synchronized(this) with exposed reference |
External code can lock on same object | Use private lock: private final Object lock = new Object() |
| Ignoring the happens-before model | Silent visibility bugs | Use volatile, locks, or atomic classes |
Quick-reference comparison
| Language | Threads | Shared memory sync | Message passing | Lock-free |
|---|---|---|---|---|
| Java | Thread, virtual threads |
synchronized, ReentrantLock |
BlockingQueue |
AtomicInteger, CAS |
| Go | Goroutines | sync.Mutex, sync.RWMutex |
Channels | sync/atomic |
| Python (CPython) | threading (GIL) |
threading.Lock |
queue.Queue |
N/A (GIL) |
| Python CPU-bound | multiprocessing |
multiprocessing.Lock |
multiprocessing.Queue |
N/A |
| Rust | std::thread |
Mutex<T>, RwLock<T> |
std::sync::mpsc |
std::sync::atomic |
| JavaScript | Web Workers | SharedArrayBuffer + Atomics |
postMessage |
Atomics.compareExchange |
FAQ
Q: What is the difference between start() and run() in Java?start() creates a new thread and calls run() in that thread. Calling run() directly executes it in the current thread — no new thread is created.
Q: Can a thread hold multiple locks simultaneously?
Yes — Java's synchronized is reentrant (same thread can acquire the same lock multiple times). With ReentrantLock, the lock count tracks each acquisition and requires matching unlock() calls.
Q: Why use notifyAll() instead of notify()?notify() wakes one arbitrary thread. If that thread's condition isn't met, it goes back to waiting — and no other thread is woken. notifyAll() wakes all waiters so the correct one can proceed.
Q: What is the difference between parallelStream() and ExecutorService?parallelStream() uses the common ForkJoinPool and is best for CPU-bound data-parallel operations on collections. ExecutorService gives you full control over pool size, task types, and lifecycle — better for I/O tasks or mixed workloads.
Q: Is StringBuilder thread-safe?
No. Use StringBuffer (synchronised, slower) or build strings locally per thread then combine.
Q: When should you use CompletableFuture vs reactive streams?CompletableFuture is great for composing a bounded number of async operations. Reactive streams (Reactor, RxJava) add back-pressure and are better for high-volume, streaming data pipelines or when the number of items is unbounded.