JavaConcurrencyintermediate
Updated:

Java Concurrency Interview Questions and Answers

6 min read

The concurrency questions Java interviews rely on — race conditions, volatile vs synchronized, happens-before, locks, atomics and deadlock — answered with code.

TL;DR – Quick Answer

Concurrency interviews test whether you can reason about shared mutable state: race conditions, the difference between visibility (volatile) and atomicity (synchronized), the happens-before rule of the Java Memory Model, locks and atomics, executors over raw threads, and how deadlock forms and is prevented. Interviewers grade you on separating atomicity from visibility and on preferring higher-level constructs to hand-rolled synchronization.

On This Page

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?
volatile guarantees visibility — a write is seen by other threads immediately — but not atomicity of compound actions like count++. synchronized guarantees both visibility and mutual exclusion, so only one thread executes the block at a time. Use volatile for a simple flag; synchronized (or an atomic) for read-modify-write.
What is a race condition?
A race condition occurs when the correctness of a program depends on the timing or interleaving of threads accessing shared mutable state. The classic example is two threads running count++ concurrently; because it is read-modify-write, updates can be lost. The fix is to make the operation atomic or mutually exclusive.
What is the happens-before relationship?
Happens-before is the Java Memory Model rule that defines when one thread's writes are guaranteed visible to another. Unlocking a monitor happens-before locking it, a volatile write happens-before a subsequent read of it, and thread start/join establish ordering. Without a happens-before edge, one thread may not see another's writes.
Why prefer the Executor framework over creating threads directly?
Executors decouple task submission from thread management, reuse threads via pools, bound resource usage, and provide Future results and scheduling. Creating a new Thread per task is unbounded and expensive; a thread pool controls concurrency and degrades gracefully under load.
How do you prevent deadlock?
The most reliable technique is to acquire locks in a consistent global order across all threads, so a cycle cannot form. Other tactics include using tryLock with a timeout, holding locks for the shortest time possible, and reducing the number of locks by using higher-level concurrent collections.

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

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 16 July 2026 LinkedIn
Chat with us