Java Multithreading Interview Questions (JMM, Locks & Traps) — Freshers to Senior
A senior-level Java concurrency interview guide covering the Java Memory Model, visibility vs atomicity, volatile/synchronized/atomic, CAS, ReentrantLock, wait-notify, deadlock and coordination tools, ThreadPoolExecutor internals, CompletableFuture, double-checked locking, and Java 21 virtual threads.
Multithreading is the round that separates mid-level from senior. Anyone can start a thread; the hard questions are about the Java Memory Model — why one thread doesn't see another's write, what volatile actually guarantees (and what it doesn't), why i++ isn't atomic, and how to reason about happens-before. This is a deep concurrency interview guide: visibility and reordering, synchronized vs volatile vs atomics and CAS, the executor/thread-pool internals, deadlock and the classic coordination tools, correct wait/notify, double-checked locking, and Java 21 virtual threads.
volatile fixes visibility but not atomicity; synchronized/locks fix both. Say that clearly and you're ahead of most candidates.The Java Memory Model — the core
Q1. What problem does the JMM solve? Explain happens-before.
The compiler, JIT, and CPU may reorder instructions and cache values in registers/CPU caches for speed. Without rules, one thread's write might never become visible to another. The JMM defines happens-before: if action A happens-before B, A's effects are guaranteed visible to B. Key edges: unlocking a monitor happens-before locking it; a volatile write happens-before a subsequent volatile read; Thread.start() happens-before the thread's run; the thread's actions happen-before join() returns.
Q2. Predict the problem — the visibility bug.
boolean running = true; // NOT volatile // Thread A while (running) { /* work */ } // may loop FOREVER // Thread B running = false; // A may never see this write
Without volatile, the JIT can hoist running into a register and never re-read it — an infinite loop even though B set it false. Marking running volatile forces a fresh read from main memory each iteration. This is the canonical visibility question.
Q3. volatile vs synchronized vs Atomic — what does each guarantee?
| Visibility | Atomicity | Blocking? | |
|---|---|---|---|
volatile | Yes | No (not for x++) | No |
synchronized | Yes | Yes | Yes (mutual exclusion) |
AtomicInteger (CAS) | Yes | Yes (single var) | No (lock-free) |
Q4. Why isn't i++ atomic, and how do you fix it?
// i++ is really 3 ops: read i, add 1, write i. // Two threads can interleave and lose an update (race condition). AtomicInteger c = new AtomicInteger(); c.incrementAndGet(); // atomic via CAS — no lock, no lost updates
CAS (compare-and-swap) reads the current value, computes the new one, and swaps only if the value is unchanged — retrying in a loop otherwise. It's a hardware instruction, so it's lock-free and fast under moderate contention. The subtle flaw is the ABA problem (value goes A→B→A and CAS wrongly succeeds), solved by AtomicStampedReference.
Locks & coordination
Q5. synchronized vs ReentrantLock — when reach for the Lock?
synchronized is simpler and auto-releases on exit/exception, but it's all-or-nothing. ReentrantLock adds: tryLock() (non-blocking / with timeout — avoids deadlock), fairness policy, interruptible lock acquisition, and multiple Condition objects (separate wait-sets). Cost: you must unlock() in a finally. Use synchronized by default; reach for ReentrantLock when you need those features. ReadWriteLock allows many concurrent readers but exclusive writers — great for read-heavy caches.
Q6. Explain wait/notify. Why must wait() be in a loop and inside synchronized?
synchronized (lock) { while (!condition) lock.wait(); // loop, NOT if // ... proceed }
wait()/notify() require holding the object's monitor (else IllegalMonitorStateException). You loop rather than if because of spurious wakeups (a thread can wake without a notify) and because another thread may have invalidated the condition between the notify and reacquiring the lock — so you must recheck. Prefer notifyAll() over notify() to avoid lost-wakeup bugs with multiple waiters.
Q7. Deadlock — the four conditions and how to prevent it.
Deadlock needs all four (Coffman conditions): mutual exclusion, hold-and-wait, no preemption, and circular wait. Break any one. The practical fix is to eliminate circular wait by acquiring locks in a global order, or use tryLock with timeout and back off. Related pathologies: livelock (threads keep responding to each other, no progress) and starvation (a thread never gets the CPU/lock — fairness helps).
Q8. CountDownLatch vs CyclicBarrier vs Semaphore?
| Tool | Purpose | Reusable? |
|---|---|---|
CountDownLatch | Wait until N events complete (one-shot gate) | No |
CyclicBarrier | N threads wait for each other at a barrier | Yes |
Semaphore | Limit concurrent access to N permits | Yes |
Thread pools & executors
Q9. Why use ExecutorService instead of new Thread()?
Creating a thread per task is expensive (each ~1MB stack) and unbounded — a load spike can exhaust memory. A thread pool reuses a fixed set of workers, bounds concurrency, and decouples task submission from execution. Prefer ExecutorService / ThreadPoolExecutor and submit Runnable/Callable.
Q10. Explain ThreadPoolExecutor's core parameters.
new ThreadPoolExecutor( corePoolSize, // kept alive even when idle maximumPoolSize, // max workers under load keepAliveTime, // idle timeout for extra threads unit, workQueue, // tasks wait here when all core threads busy threadFactory, rejectionHandler); // what to do when queue+pool are full
Flow: fill core threads → queue tasks → if the queue is full, spin up to max → if still full, apply the rejection policy (AbortPolicy throws, CallerRunsPolicy runs it on the caller, DiscardPolicy drops it). The Executors.newFixedThreadPool factories are convenient but use an unbounded queue — a memory risk; senior answer: configure a bounded queue and rejection policy explicitly.
Q11. Future vs CompletableFuture?
Future.get() blocks and can't be chained. CompletableFuture (Java 8) is non-blocking and composable — thenApply, thenCompose, thenCombine, exceptionally — letting you build async pipelines without blocking a thread per stage.
Safe publication & the singleton trap
Q12. Double-checked locking — why is volatile essential?
private static volatile Singleton inst; // volatile is MANDATORY static Singleton get() { if (inst == null) { synchronized (Singleton.class) { if (inst == null) inst = new Singleton(); } } return inst; }
Without volatile, inst = new Singleton() can be reordered — the reference is published before the constructor finishes, so another thread sees a non-null but half-built object. volatile forbids that reordering. (In practice, an enum singleton or a static holder class is simpler and safe by construction.)
Q13. What makes a class thread-safe?
No shared mutable state (stateless), or all mutable state is confined (e.g. ThreadLocal), immutable (final fields, no setters — inherently safe), or guarded by a consistent lock. Immutability is the cheapest thread-safety: an immutable object can be shared freely with no synchronisation.
Java 21 — virtual threads
Q14. What are virtual threads and why do they matter?
Virtual threads (Project Loom, stable in Java 21) are lightweight threads managed by the JVM, not the OS. Millions can run concurrently: a blocking call unmounts the virtual thread from its carrier OS thread instead of blocking it, so I/O-bound servers scale without the complexity of async/reactive code. Write simple blocking code, get async scalability. See virtual threads explained.
Rapid-fire
- Thread states? NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.
- Runnable vs Callable? Callable returns a value and can throw checked exceptions; Runnable can't.
- sleep() vs wait()?
sleepholds the lock and pauses the thread;waitreleases the monitor and waits for notify. - start() vs run()?
start()spawns a new thread; callingrun()directly just executes on the current thread. - Can you restart a dead thread? No —
start()twice throwsIllegalThreadStateException. - Daemon thread? A background thread the JVM won't wait for on shutdown (e.g. GC).
Go deeper by topic
- Core Java interview questions — the backbone set
- Collections interview questions — why ConcurrentHashMap is safe
- OOP interview questions — immutability & encapsulation for thread safety
- Java virtual threads — the modern concurrency model
- Spring Boot interview questions — @Async, thread pools in practice
Concurrency stopped scaring me the day I split every question into two: "is this a visibility problem or an atomicity problem?" The stuck-loop bug is pure visibility — fixed by volatile. The lost-counter bug is atomicity — volatile won't save it; you need a lock or an atomic. Once you carry that lens, the whole toolkit falls into place: synchronized and locks give you both, atomics give you lock-free atomicity on one variable, immutability sidesteps the question entirely. Reason from the memory model and happens-before, not from memorised rules, and you'll answer the follow-ups interviewers use to find the people who actually understand threads. That understanding is rare — and it's exactly what gets senior offers. You've got this.
Related
← Collections interview questions · Exception interview questions →