Why concurrency separates strong Java developers
Concurrency is where Java interviews find the ceiling of a candidate's understanding. Anyone can start a thread; far fewer can explain why a counter loses updates, why a flag written by one thread is never seen by another, or how a deadlock forms. Interviewers lean on concurrency because the reasoning — separating atomicity from visibility, thinking about interleavings — does not come from memorization. It comes from actually understanding the model.
This page covers the concurrency questions asked most, from race conditions to the memory model to executors. It builds on the runtime described in the JVM questions and the heap/stack split in the memory management set.
Q1. What is a race condition?
A race condition is when a program's correctness depends on the timing of threads accessing shared mutable state. The textbook case is count++ run by two threads: it is really read-modify-write (three steps), so two threads can read the same value, both increment, and one update is lost.
class Counter {
private int count = 0;
void increment() { count++; } // NOT atomic: read, add, write — updates can be lost
}
The fix is to make the operation atomic or mutually exclusive — synchronized, an explicit lock, or AtomicInteger. The key insight interviewers want is why count++ is unsafe: because it is three operations, not one, and the JVM can interleave two threads between them.
Q2. What is the difference between volatile and synchronized?
volatile guarantees visibility: a write to a volatile field is immediately visible to other threads, and reads are never cached in a way that hides updates. It does not make compound actions atomic. synchronized guarantees both visibility and mutual exclusion, so a whole read-modify-write block runs as one unit.
volatile boolean running = true; // fine: single write, single read — visibility only
void stop() { running = false; } // other threads see it promptly
// But volatile is NOT enough for count++ — that needs atomicity:
private final AtomicInteger count = new AtomicInteger();
void increment() { count.incrementAndGet(); } // atomic read-modify-write
The separation of visibility from atomicity is the single most important concurrency distinction, and it is where most candidates stumble. volatile for a stop flag; synchronized or an atomic for a counter. Getting that boundary right is what marks a real understanding.
Q3. Explain the happens-before relationship.
Happens-before is the Java Memory Model rule that defines when one thread's writes are guaranteed visible to another. Key edges: releasing a lock happens-before acquiring it; a volatile write happens-before a later read of that field; Thread.start() happens-before the thread's code; and a thread's actions happen-before another thread's join() on it.
Without a happens-before edge between two actions, the JVM and CPU are free to reorder and cache, so one thread may simply never observe another's write — even a write that "obviously" happened first in source order. Framing correctness as "establish a happens-before edge" rather than "add some synchronization and hope" is the senior mental model.
Q4. What atomic classes does Java provide, and how do they work?
java.util.concurrent.atomic offers AtomicInteger, AtomicLong, AtomicReference and others that provide lock-free atomic operations via a compare-and-swap (CAS) instruction. CAS reads the current value and updates it only if it has not changed, retrying on contention — giving thread-safe updates without blocking.
AtomicReference<State> state = new AtomicReference<>(State.IDLE);
state.compareAndSet(State.IDLE, State.RUNNING); // succeeds only if still IDLE
The trade-off to mention: CAS is fast under low contention but spins/retries under high contention, so atomics beat locks for simple counters and flags but not for protecting large multi-step invariants. Knowing CAS underlies these classes — and the ABA problem it can face — is the advanced layer.
Q5. Why use the Executor framework instead of raw threads?
Executors separate what to run from how threads are managed: they reuse threads via pools, bound concurrency, queue excess work, and return Future results. Creating a new Thread per task is unbounded — under load it exhausts memory and the OS — while a pool caps resource use and degrades gracefully.
ExecutorService pool = Executors.newFixedThreadPool(8);
Future<Integer> result = pool.submit(() -> expensiveComputation());
Integer value = result.get(); // blocks until the task completes
pool.shutdown();
The senior addition is bounding the queue: an unbounded work queue turns overload into an OutOfMemoryError instead of back-pressure, so a real production pool uses a bounded queue and a rejection policy. Preferring executors and CompletableFuture over hand-managed threads is exactly the modern practice interviewers look for.
Q6. How does deadlock form, and how do you prevent it?
Deadlock needs four conditions together: mutual exclusion, hold-and-wait, no preemption, and circular wait. The everyday cause is two threads acquiring two locks in opposite orders. The most reliable prevention is imposing a single global lock-ordering so a cycle can never form; tryLock with a timeout is a fallback.
// Deadlock risk: thread A locks x then y; thread B locks y then x
// Fix: every thread acquires locks in the SAME order (e.g., by identity/hash)
void transfer(Account a, Account b, long amount) {
Account first = a.id < b.id ? a : b; // consistent global order
Account second = a.id < b.id ? b : a;
synchronized (first) {
synchronized (second) { /* move funds */ }
}
}
The account-transfer example is the canonical answer because it shows the fix — ordering locks by a stable key — rather than just naming the four conditions. Mentioning that a thread dump reveals a deadlock (each thread holding one lock, waiting for the other) shows you can also detect it.
Q7. What concurrent collections should you know?
ConcurrentHashMap for shared maps (fine-grained locking, atomic compute/merge/putIfAbsent); CopyOnWriteArrayList for read-heavy, rarely-written lists like listeners; and the BlockingQueue family (ArrayBlockingQueue, LinkedBlockingQueue) for producer-consumer pipelines, where put/take block on full/empty.
The choosing principle is read/write ratio and coordination need: ConcurrentHashMap for general shared state, copy-on-write only when reads vastly outnumber writes, and blocking queues to hand work between producers and consumers with built-in back-pressure. Reaching for these instead of synchronized around a plain HashMap is the idiomatic answer.
Q8. What is the difference between wait/notify and higher-level tools?
wait(), notify() and notifyAll() are the low-level monitor primitives — error-prone because they require holding the lock, looping on the condition, and choosing notify versus notifyAll correctly. Higher-level tools — BlockingQueue, CountDownLatch, CyclicBarrier, Semaphore, CompletableFuture — express the same coordination more safely and are preferred.
The rule to state: always call wait() inside a while loop checking the condition (to handle spurious wakeups), and prefer a purpose-built concurrency utility over hand-rolled wait/notify whenever one fits. Knowing that wait releases the lock while sleeping — unlike Thread.sleep — is the detail that catches people, and answering it cleanly signals real understanding.
How to prepare
Drill the two distinctions everything hangs on: atomicity versus visibility (synchronized versus volatile) and the happens-before rule — if you can explain why count++ loses updates and why a non-volatile flag is never seen, you can derive most answers. Write the demos: the lost-update counter, the two-lock deadlock, the fix by lock ordering. Prefer java.util.concurrent — executors, atomics, concurrent collections — over raw threads in every answer, since that is current practice. Connect this to the JVM questions for the runtime and the memory management set for the shared heap, then pressure-test your reasoning under follow-ups in a mock interview.
Frequently Asked Questions
What is the difference between volatile and synchronized?
What is a race condition?
What is the happens-before relationship?
Why prefer the Executor framework over creating threads directly?
How do you prevent deadlock?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — View the Java Full Stack curriculum

