Synchronization: interview questions & scenarios
These are the synchronization questions I ask most, in roughly the order I ask them. They start with "what's a race condition?" and, within ten minutes, turn into "why is your service frozen at 0% CPU?" Every answer is correct for Java 21, every output block comes from a real run, and I flag it where a newer JDK behaves differently.
Here's how to use this page: read the question, answer it out loud first, then open the model answer. Don't skip the "What if…" chains. That's where most candidates run out of road, and where you get to show real depth. If an answer feels thin, the lessons go deeper: Race conditions, synchronized and monitors, Reentrancy, wait, notify and notifyAll and The producer-consumer problem.
What is a race condition? Give me a real example.
Tests race condition, interleaving, read-modify-write, check-then-act
Show answerHide answer
Model answer
A race condition is a bug where the result depends on the timing of threads, and nobody controls that timing. Run the code a thousand times and it's right 999 times. That's what makes it nasty.
Picture two cashiers sharing one paper ledger. Both read "100", both add their own sale, both write their answer back. One sale vanishes. Nobody did anything wrong on their own; the order of their steps did the damage.
Almost every race you'll meet in Java is one of two shapes:
| Shape | What it looks like | Why it breaks |
|---|---|---|
| Read-modify-write | count++, balance = balance - amount |
Two threads read the same old value, and one write overwrites the other |
| Check-then-act | if (!map.containsKey(k)) map.put(k, v), lazy init with if (instance == null) |
The fact you checked can become false before you act on it |
// check-then-act on a bank account
if (account.balance >= 100) { // thread A checks: 150, fine
// thread B checks: 150, fine
account.balance -= 100; // A: 50
} // B: -50. Overdrawn.
The fix is always the same idea: make the check and the act (or the read and the write) one indivisible step, with synchronized, a Lock, or an atomic operation that does both at once.
How I’d say it in the interview
"A race condition is when the correctness of my code depends on how threads happen to interleave. The classic shapes are read-modify-write, like count plus plus, and check-then-act, like 'if the key isn't there, put it'. Both break because another thread sneaks in between my two steps. The fix is to make those steps atomic, with a lock or an atomic operation."
Signals depth
Naming the two shapes (read-modify-write and check-then-act) instead of only saying "two threads touch the same data". Pointing out that the bug lives between two operations that are each fine on their own, which is why making every field volatile doesn't fix it.
Red flags
"A race condition is when two threads run at the same time." Running at the same time is the whole point of threads; the problem is uncontrolled interleaving on shared, mutable state. Also: "it only happens on multi-core machines". A single core can still switch threads between your read and your write.
Follow-ups interviewers ask next
What if the shared state is only ever read, never written?
Then there's no race. Immutable, safely published data can be read by any number of threads with no locking at all. That's why "make it immutable" is the first fix I reach for, before any lock.
What if the state is only written by one thread and read by others?
No lost updates, because only one thread writes. But you can still have a visibility problem: readers may keep seeing an old value. One writer plus many readers is the classic case for
volatile, which we cover in the planned "volatile" lesson.
Why isn't count++ thread-safe? What does this print?
Tests lost updates, non-atomic increment, bytecode
Show answerHide answer
Model answer
public class LostUpdates {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable work = () -> {
for (int i = 0; i < 1_000_000; i++) count++;
};
Thread a = new Thread(work);
Thread b = new Thread(work);
a.start(); b.start();
a.join(); b.join();
System.out.println("expected 2000000, got " + count);
}
}
Five real runs on Java 21 (java LostUpdates.java, 14-core laptop):
expected 2000000, got 1026781
expected 2000000, got 1017385
expected 2000000, got 1016983
expected 2000000, got 1024829
expected 2000000, got 1012349
The honest answer to "what does it print?" is "some number up to 2,000,000, different every run". Here almost half the increments vanished. On another machine you might lose 5% or 60%. On a rare run you could even get exactly 2,000,000.
Why? count++ looks like one step, but it's three. Here's the real bytecode from javap -c:
getstatic #28 // Field count:I (1. read count)
iconst_1
iadd (2. add one)
putstatic #28 // Field count:I (3. write it back)
If thread A reads 41 and thread B reads 41 before either writes, both write 42. Two increments, one result. That's a lost update.
Three fixes, simplest first:
static final AtomicInteger count = new AtomicInteger(); // count.incrementAndGet()
static synchronized void inc() { count++; } // a lock around all three steps
static final LongAdder hits = new LongAdder(); // hits.increment(), best under heavy contention
How I’d say it in the interview
"It prints some number less than two million, and a different one each time. count plus plus is really three steps: read, add, write. Two threads can read the same value, both add one, and both write the same result back, so increments get lost. I'd fix it with an AtomicInteger, or a synchronized method, or a LongAdder if lots of threads are hammering one counter."
Signals depth
Saying "the output is nondeterministic" instead of guessing a number, and showing the read-add-write split (bonus for knowing the bytecode). Mentioning that the join() calls matter: without them, main could print before the workers even finish, which is a second bug hiding inside the first.
Red flags
"It prints 2000000." Or "make count volatile". volatile makes each read and write visible, but it doesn't glue the three steps together, so you still lose updates.
Follow-ups interviewers ask next
What if you make count volatile? Does the number go up?
You still lose updates, because the read and the write are still separate steps. The number is still wrong and still random.
volatilefixes seeing the latest value, not atomically updating it.What if you swap AtomicInteger for LongAdder? When is that better?
AtomicIntegeruses one memory slot, and every thread retries a compare-and-swap on it. With dozens of threads hammering it, those retries pile up.LongAdderspreads the count across several cells and adds them up only when you callsum(). It's much faster for write-heavy counters like metrics. The catch:sum()isn't a snapshot, so it's not for "take the next ticket number" logic.
What exactly does synchronized lock? An instance method, a static method, and a block.
Tests monitors, intrinsic locks, this vs Class object
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
Two threads call the same synchronized instance method, on two different objects. Do they block each other?
Tests lock identity, instance vs static locks
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
What's the difference between a race condition and a data race?
Tests JLS data race definition, happens-before, atomicity vs visibility
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
A teammate caches users in a ConcurrentHashMap. Under load, some users get created twice. Here's the code. What's wrong?
Tests check-then-act, computeIfAbsent, merge, compound actions
Model answer, how to say it, 2 follow-ups: unlock with the Java Pass
This counter's increment() is synchronized but get() isn't. A reader thread spins on get() and never sees the update. Why?
Tests visibility, happens-before, JIT hoisting
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
What's wrong with each of these locks?
Tests lock identity, interned strings, value-based classes, reassigned lock fields
Model answer, how to say it, 2 follow-ups: unlock with the Java Pass
When would you use a synchronized block instead of a synchronized method? How big should the locked region be?
Tests lock granularity, critical sections, lock splitting, private locks
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
A thread throws a RuntimeException inside a synchronized block. What happens to the lock? What does this print?
Tests monitor release on abrupt completion, ReentrantLock and finally
Model answer, how to say it, 2 follow-ups: unlock with the Java Pass
Is synchronized reentrant? What would break if it weren't?
Tests reentrancy, hold count, inheritance and super calls
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
What does this print? A method takes a ReentrantLock twice but unlocks once.
Tests ReentrantLock hold count, unlock discipline, ownership
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
What's the difference between wait() and sleep()? Which one releases the lock?
Tests wait vs sleep, monitor release, waking up
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
Why must wait() and notify() be called from inside a synchronized block?
Tests lost wake-up, atomic check-and-wait, IllegalMonitorStateException
Model answer, how to say it, 2 follow-ups: unlock with the Java Pass
What is a spurious wake-up? Why do we always wait in a while loop?
Tests spurious wake-ups, guarded waits, stolen notifications
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
A producer-consumer queue uses notify() instead of notifyAll(). Two producers, two consumers, capacity 1. Sometimes everything freezes at 0% CPU. Walk me through why.
Tests notify vs notifyAll, single wait set, liveness, thread dumps
Model answer, how to say it, 2 follow-ups: unlock with the Java Pass
Implement a bounded blocking queue using only synchronized, wait() and notifyAll().
Tests guarded waits, circular buffer, notifyAll, Condition, interrupts
Model answer, how to say it, 3 follow-ups: unlock with the Java Pass
How would you actually build producer-consumer in a production service?
Tests BlockingQueue, bounded queues, backpressure, executors
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
How do you shut down a group of consumer threads that are blocked on take()?
Tests poison pill, interrupts, graceful vs immediate shutdown
Model answer, how to say it, 2 follow-ups: unlock with the Java Pass
Your cache uses a ReentrantReadWriteLock. A method reads under the read lock, then takes the write lock to update. It hangs. Why?
Tests ReadWriteLock, lock upgrade, downgrade, self-deadlock
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
synchronized or ReentrantLock: which do you reach for, and why?
Tests Lock API, tryLock, lockInterruptibly, fairness, Conditions
Model answer, how to say it, 1 follow-up: unlock with the Java Pass
You moved a service to virtual threads on Java 21. Throughput is worse than with a thread pool. A synchronized block wraps an HTTP call. What's going on, and does Java 24 change it?
Tests virtual threads, carrier threads, pinning, JEP 491, jdk.tracePinnedThreads
Model answer, how to say it, 2 follow-ups: unlock with the Java Pass
Premium question set
Keep reading with the Java Pass
The rest of this question set is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
From ₹299 · one-time payment · No auto-renewal