Skip to content
CalliCoder

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.

Two thread timelines showing a lost update from a non-atomic increment, and a reader that never observes a write, with volatile, AtomicInteger and synchronized mapped to which problem each one solves.
Two different bugs. Only a lock covers both, which is why synchronized needs no volatile.

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.