Java Multithreading Interview Questions with Answers
Published Updated Interview Questions 12 min read
The questions that separate someone who has read about synchronized from someone who has debugged a race: visibility versus atomicity, what volatile does not do, and where virtual threads change the answer.
Concurrency interviews have a tell. Ask what synchronized does and almost everyone says “it locks
the method”. Ask what happens without it and the answers split: one group says two threads might
interleave, the other says a thread might never see the other’s write at all. Only the second group
has met the Java Memory Model.
That distinction, between atomicity and visibility, is what nearly every question below is really about. Virtual threads changed the cost of blocking, not the rules of memory, so the classics still decide the interview.
Written against Java 21, where virtual threads left preview.
1. Atomicity is not visibility
The question: why is count++ unsafe across threads?
Because it is three operations: read, add, write. Two threads can read the same value and both write back the same increment, losing one. That is the atomicity answer and it is half the story.
The follow-up: if only one thread writes and the others only read, is it safe now?
No, and this is where the interview turns. Without synchronisation there is no guarantee a reader
ever observes the write. The JVM, the compiler and the CPU are all permitted to reorder and cache,
and a reader thread may hoist the read out of a loop and spin on a stale value indefinitely. The
classic demonstration is a boolean running flag that a second thread sets to false and the loop
never notices.
Fixing atomicity does not fix visibility, and fixing visibility does not fix atomicity. They are separate problems that both require the same tool: a happens-before relationship.
2. What volatile actually promises
The question: what does volatile do?
It guarantees visibility and ordering. A write to a volatile field is visible to any thread that
subsequently reads it, and reads and writes are not reordered across it. It fixes the stuck
running flag.
The follow-up: does it make count++ safe? No. volatile gives no atomicity for compound
actions. Read-modify-write on a volatile field is still three steps and still loses updates. That is
what AtomicInteger is for, and its incrementAndGet is a single compare-and-swap loop.
The rule that survives the interview: volatile for a flag one thread writes and others read;
Atomic* for a counter anyone updates; a lock when several fields must change together.
3. synchronized, and what it is locking
The question: what does synchronized guarantee?
Mutual exclusion and, just as importantly, a happens-before edge. Everything a thread did before
releasing a monitor is visible to the next thread that acquires the same monitor. That second half
is why synchronized also solves the visibility problem, and why a correctly synchronised counter
needs no volatile.
The follow-up: which object is being locked? On an instance method, this. On a static method,
the Class object. Two threads calling a synchronized instance method on different instances
contend for nothing, which surprises people who expected the method itself to be serialised.
The follow-up after that is usually about locking on the wrong thing. Synchronising on a field that
gets reassigned means later threads lock a different object and the exclusion silently evaporates.
Synchronising on a String literal or a boxed Integer is worse, because the JVM interns them and
unrelated code can lock the same instance.
4. Deadlock, and the fix that is not a fix
The question: what causes deadlock and how do you avoid it?
Two threads each holding a lock the other needs. The reliable prevention is lock ordering: if
every code path acquires locks in one global order, a cycle cannot form. Where an order is hard to
define, tryLock with a timeout turns a deadlock into a failure that can be retried and logged.
The follow-up: is tryLock a fix? Not on its own. It converts deadlock into livelock if the
retry loop has no backoff, and two threads politely releasing and re-acquiring in lockstep make no
more progress than two threads stuck. A timeout needs jitter, and it needs a bound on retries.
5. wait, notify, and why nobody should write them any more
The question: what is the difference between wait() and sleep()?
sleep holds every lock it has. wait releases the monitor it was called on and reacquires it
before returning. That is why wait must be called inside synchronized on the same object.
The follow-up: why is wait always inside a while and never an if? Spurious wakeups are
permitted by the specification, and with notifyAll several waiters wake for one available item.
The condition must be rechecked after waking, which a while does and an if does not.
The honest answer to the whole family is that application code should use java.util.concurrent
instead. A BlockingQueue expresses producer-consumer directly, and CountDownLatch, Semaphore
and CompletableFuture cover most of the rest. Hand-rolled wait/notify in a code review is
usually a defect waiting for a schedule.
6. Thread pools, and the parameter that causes outages
The question: why use an ExecutorService rather than creating threads?
Thread creation costs, and an unbounded number of platform threads exhausts memory. A pool bounds concurrency and reuses threads.
The follow-up: what is wrong with Executors.newFixedThreadPool(n)? Its queue is unbounded. Work
arriving faster than it is processed accumulates until the heap is gone, and the failure looks like a
memory leak rather than an overload. The same is true of newCachedThreadPool in the other
direction: its pool is unbounded, so a burst creates thousands of threads.
Constructing a ThreadPoolExecutor directly, with a bounded queue and an explicit rejection policy,
is the answer that shows production experience. CallerRunsPolicy in particular gives free
backpressure: when the queue is full the submitting thread runs the task itself and stops accepting
new work while it does.
7. Where virtual threads change the answer, and where they do not
The question: do virtual threads make this obsolete?
They change the economics of blocking. A virtual thread that blocks on IO unmounts from its carrier instead of holding an OS thread, so a thread-per-request design scales to numbers that platform threads never could. Pooling virtual threads is pointless; they are cheap enough to create per task.
The follow-up: what does not change? Every memory rule above. Races, visibility and deadlock behave identically, because virtual threads are threads. Two of them incrementing an unsynchronised counter lose updates exactly as before.
One genuinely new hazard is worth naming: a virtual thread inside a synchronized block that blocks
cannot always unmount, so it pins its carrier thread. Under load that can starve the carrier pool.
Replacing synchronized with a ReentrantLock in code that blocks while holding it is the standard
remedy, and it is a specific, recent answer that shows the candidate has read something since 2019.
How to answer
Every question in this list has a one-sentence answer and a second layer, and the second layer is
the interview. Say what the construct guarantees, then say what it does not. “volatile gives
visibility, not atomicity” is worth more than a paragraph on cache coherency.
When the answer depends on the memory model, say so in those words. Interviewers are listening for whether a candidate reasons about happens-before or about what usually happens on their laptop, and the two produce the same output right up until production load.
Frequently asked questions
Is volatile enough for a counter?
No. It makes writes visible but leaves read-modify-write
non-atomic. Use AtomicInteger, or a lock if other fields change with it.
What is happens-before?
The ordering guarantee that makes one thread’s writes visible to another. Releasing a monitor happens-before acquiring it, writing a volatile happens-before reading it, and starting a thread happens-before anything it runs.
Does synchronized guarantee visibility as well as exclusion?
Yes, and that is the half people
forget. A correctly synchronised field does not additionally need volatile.
Why prefer ReentrantLock over synchronized?
For tryLock, timeouts, interruptible acquisition,
fairness and multiple condition variables. Also, in Java 21, because a blocking virtual thread can
pin its carrier inside synchronized.
Can two threads deadlock on one lock?
Not on one lock with reentrant locking, since a thread can reacquire a monitor it already holds. Deadlock needs at least two locks and a cycle.
Is HashMap safe if only one thread writes?
No. Concurrent reads during a resize can see a
corrupted structure. Use ConcurrentHashMap.
What does ConcurrentHashMap actually lock?
Individual bins rather than the whole table, so
unrelated keys do not contend. Iteration is weakly consistent: it never throws
ConcurrentModificationException and may or may not reflect writes made after it started.
Should virtual threads be pooled?
No. They are cheap to create, and pooling reintroduces the
bound they exist to remove. Use Executors.newVirtualThreadPerTaskExecutor().
How do I bound concurrency with virtual threads?
With a Semaphore around the shared resource,
not by limiting threads. The limit belongs on the thing that is scarce, such as connections.
What is thread starvation?
A thread that is runnable but never scheduled, usually because others hold the lock it needs or the pool is saturated by long tasks. Bounded queues and separate pools for slow work are the usual remedies.