Skip to main content

Top 40 Java Concurrency Interview Questions and Answers (2026)

40 Java concurrency interview questions, from race conditions to ScopedValue’s exact child-thread inheritance rules. The four trickiest get fresh, verified code and real captured output; the rest link to this site’s own deep dives that already proved their claims.

“Just mark it volatile” is one of the most confidently wrong answers given in Java concurrency interviews – and it’s wrong in a way that’s easy to demonstrate rather than just assert. Below, a plain int and a volatile int run through the identical concurrent-increment workload as an AtomicInteger, and only one of the three comes out with the right count. That’s the approach for this whole list: 40 questions spanning classic Java concurrency through JDK 25/26’s structured concurrency and scoped values, with real code and real output behind the answers that are easy to get subtly wrong, and links to this site’s own deep, verified companion posts for the ones that already got the full treatment elsewhere.
Versions. JDK 25.0.4.1+1 (Temurin, LTS) throughout. ScopedValue is finalized in JDK 25 (JEP 506); StructuredTaskScope is still a preview API as of JDK 25/26 (JEP 505/525), so anything using it here is compiled and run with --enable-preview. JUnit 5.11.0 for the correctness tests. On a 2-vCPU x86-64 virtual machine – the same sandbox as the rest of this series.
Four of these answers get fresh, verified code in a new module of this site’s companion repo: a real race condition with its actual lost-update count, why volatile alone doesn’t fix it, a real JVM-detected deadlock, and exactly which threads a ScopedValue binding does and doesn’t reach. The rest link to this site’s own posts that already proved their claims with real benchmarks, JFR events, and captured output – this is a hub, not a re-derivation.

Concurrency fundamentals (Q1-Q6)

Q1. What is a race condition?

A race condition happens when the correctness of a result depends on the unpredictable timing or interleaving of multiple threads accessing shared, mutable state. The classic example is two threads both reading a counter’s current value before either writes back – whichever writes last “wins,” and the other thread’s update is silently lost.

Q2. What does “thread-safe” actually mean?

A class or method is thread-safe if it behaves correctly – no data corruption, no lost updates, no observed intermediate state – when called concurrently by multiple threads, with no additional synchronization required by the caller. “Behaves correctly” has to include every possible interleaving the scheduler might choose, not just the ones a quick test happened to hit.

Q3. What is a deadlock, and how do you actually detect one?

A deadlock is a cycle of threads each waiting on a resource the next thread in the cycle already holds, so none of them can ever proceed. The textbook cause is two threads acquiring two locks in opposite order. This is verified directly rather than just described: DeadlockDemo below starts thread-1 holding lockA and waiting on lockB, and thread-2 holding lockB and waiting on lockA, then detects the resulting deadlock with the same java.lang.management API a monitoring tool or jstack would use:
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] deadlockedIds = threadBean.findDeadlockedThreads();
for (ThreadInfo info : threadBean.getThreadInfo(deadlockedIds, true, true)) {
    System.out.println(info.getThreadName() + " is blocked on " + info.getLockInfo()
            + ", owned by " + info.getLockOwnerName());
}
$ java --enable-preview DeadlockDemo

=== Opposite lock order: a real deadlock, detected via ThreadMXBean ===
thread-1: holding lockA, waiting for lockB
thread-2: holding lockB, waiting for lockA
main: findDeadlockedThreads() detected 2 deadlocked threads
  thread-1 is blocked on java.lang.Object@5451c3a8, owned by thread-2
  thread-2 is blocked on java.lang.Object@49476842, owned by thread-1

=== Same lock order on both threads: no deadlock possible ===
thread-1 (fixed order): acquired both locks, done
thread-2 (fixed order): acquired both locks, done
main: both fixed-order threads finished, isAlive t1=false t2=false
Output: 03-deadlock-detection.txt, source: DeadlockDemo.java. The fix isn’t “add more locking” – it’s making every thread that needs both locks acquire them in the same order, which makes the circular-wait condition structurally impossible, not just less likely.

Q4. What’s the difference between concurrency and parallelism?

Concurrency is about structure: multiple tasks making progress over overlapping time periods, possibly by interleaving on a single core. Parallelism is about execution: multiple tasks literally running at the same instant on different cores. A single-core machine can still run concurrent code (via time-slicing); it cannot run anything in true parallel.

Q5. What is a critical section, and how do you protect one in Java?

A critical section is the portion of code that accesses shared mutable state and must not be executed by more than one thread at a time. Java protects one with synchronized blocks or methods, an explicit Lock implementation like ReentrantLock, or by avoiding shared mutable state entirely (atomics, immutability, confinement to one thread).

Q6. Why isn’t count++ atomic, and what does that actually cost you?

count++ is read, add, write – three separate steps, not one. Two threads can both read the same value before either writes back, and one increment is lost. This is proven with a real run rather than just asserted: 8 threads each increment a plain int 200,000 times (1,600,000 total expected) next to an AtomicInteger doing identical work:
$ java --enable-preview RaceConditionDemo

expected increments: 1600000
unsafeCounter (plain int, unsynchronized): 1248496
safeCounter (AtomicInteger): 1600000
unsafeCounter lost 351504 updates to the race
safeCounter matches expected: true
Output: 01-race-condition.txt, source: RaceConditionDemo.java. 351,504 lost updates on this particular run – not “some,” a real number, and it’ll be a different real number next run. See Demystifying Thread Safety in Java for the full toolkit (synchronization, atomics, immutability, concurrent collections).

Going deeper on this section

synchronized, locks, and atomics (Q7-Q13)

Q7. What’s the difference between synchronized and ReentrantLock?

synchronized is built into the language – automatic, unconditional release even on an exception, but no timeout, no fairness option, no way to check if it’s held. ReentrantLock is an explicit API offering tryLock(timeout), optional fairness, and interruptible acquisition, at the cost of requiring a manual try/finally to guarantee release. Both were benchmarked head-to-head against StampedLock with real JMH numbers in synchronized vs ReentrantLock vs StampedLock.

Q8. What does StampedLock’s optimistic read actually buy you?

An optimistic read never blocks a writer and never blocks other readers – it just reads the data and then validates, after the fact, that no write happened in between. If validation fails, it falls back to a real read lock. On a 9:1 read-heavy workload this measured over 20x faster than a fair ReentrantReadWriteLock – see the numbers in the locks benchmark post.

Q9. What does lock fairness cost you, concretely?

A fair lock grants access in strict FIFO arrival order, which prevents starvation but requires park/unpark handoffs between threads instead of letting whichever thread happens to be running grab the lock. The locks post measured that handoff cost directly under contention rather than describing it abstractly.

Q10. How does tryLock(timeout) help avoid deadlock?

Instead of blocking forever waiting for a lock (the setup for a deadlock), tryLock(timeout, unit) gives up after a bounded wait and returns false, letting the caller back off, release what it already holds, and retry – breaking the circular-wait condition instead of just hoping it never happens. The locks post resolves a real, live deadlock this way with captured output.

Q11. What is Compare-And-Swap (CAS), and why can it beat a lock?

CAS is a hardware instruction: “update this memory location to a new value, but only if it currently holds the value I expect.” If another thread changed it first, the CAS fails and the caller retries. This is optimistic rather than pessimistic – no thread ever blocks waiting for a lock to be released, which is why AtomicInteger often outperforms synchronized under low-to-moderate contention. Full CAS-loop mechanics, including a hand-written VarHandle version, are in Java Atomics and VarHandle.

Q12. When does LongAdder beat AtomicLong?

Under high write contention, many threads hammering one AtomicLong all retry CAS against the same memory location. LongAdder spreads updates across multiple internal cells and sums them only when you call sum(), trading read cost for dramatically less write contention. Benchmarked directly in the atomics post.

Q13. What is the ABA problem?

A CAS succeeds if the current value matches the expected value – but if the value changed from A to B and back to A while a thread wasn’t looking, its CAS still succeeds even though the state genuinely changed in between. This is a real, reproducible bug against a hand-rolled Treiber stack, fixed with AtomicStampedReference, in the atomics post.

Going deeper on this section

The Java Memory Model (Q14-Q17)

Q14. What does “happens-before” mean?

“Happens-before” is the JMM’s guarantee that if action A happens-before action B, every thread is guaranteed to see A’s effects when it observes B. Without a happens-before relationship between a write on one thread and a read on another, the reading thread is not guaranteed to see the write at all, regardless of wall-clock timing. volatile writes/reads, lock acquire/release, and thread start/join all establish happens-before edges – covered from first principles in The Java Memory Model Explained.

Q15. Why does double-checked locking need volatile?

Without volatile on the singleton field, the compiler and CPU are free to reorder the constructor’s writes relative to the field assignment – another thread can observe a non-null reference to an object whose constructor hasn’t finished running yet. This isn’t a theoretical risk: the JMM post reproduces the actual broken publish with a 5+ billion sample jcstress campaign, then the volatile fix, side by side.

Q16. What guarantee do final fields actually give you?

A properly constructed object with final fields is safely published: any thread that obtains a reference to the object after construction completes is guaranteed to see the fully-initialized values of those final fields, with no synchronization required on the reader’s side. This is the mechanism behind why immutable objects are inherently thread-safe for reading – proven empirically in the JMM post’s third jcstress case.

Q17. Is volatile enough to make count++ thread-safe?

No – and this is a real interview trap, not a hypothetical one. volatile guarantees visibility (every thread sees the latest write) and prevents certain reorderings; it does nothing about the read-modify-write sequence being non-atomic. Run through the identical concurrent-increment workload as Q6, a volatile int loses updates exactly like a plain int does:
private static volatile int volatileCounter = 0;
// 8 threads, 200,000 increments each:
volatileCounter++;  // visibility guaranteed, atomicity is NOT
$ java --enable-preview VolatileNotEnoughDemo

expected increments: 1600000
volatileCounter: 855018 (volatile guarantees every thread SEES the latest value - it does not make ++ atomic)
atomicCounter: 1600000
volatileCounter lost updates: 744982
atomicCounter matches expected: true
Output: 02-volatile-not-enough.txt, source: VolatileNotEnoughDemo.java. 744,982 lost updates – worse than the plain-int run in Q6 purely by chance of scheduling, not because volatile made anything worse. The fix for a shared counter is AtomicInteger or a lock, never the volatile keyword alone.

Going deeper on this section

Collections (Q18-Q21)

Q18. Why is HashMap unsafe for concurrent modification?

HashMap does no internal synchronization at all – concurrent structural modification (resizing, bucket-to-tree conversion) from multiple threads can corrupt its internal linked-list/tree structure, in the worst historical case producing an infinite loop during resize. Its iterators are fail-fast and throw ConcurrentModificationException on detected concurrent structural change, but that’s a best-effort safety net, not a guarantee. Full internals in HashMap vs ConcurrentHashMap: Complete Interview Guide.

Q19. How does ConcurrentHashMap avoid locking the whole map?

Java 8+ synchronizes only at the level of the bucket being modified (the first node of that bucket’s chain), using CAS for the empty-bucket case, so unrelated buckets can be written concurrently by different threads with zero contention between them. Reads are entirely lock-free. Detailed in the HashMap vs ConcurrentHashMap guide.

Q20. When would you reach for CopyOnWriteArrayList?

When reads vastly outnumber writes and you want reads to never block, ever – every mutation copies the entire backing array, so reads always see a stable, unchanging snapshot with zero locking. It’s a poor fit for write-heavy lists (each write is O(n)), but ideal for something like a rarely-changed listener list read constantly. Covered in Demystifying Thread Safety in Java.

Q21. What is BlockingQueue for, and what pattern does it enable?

BlockingQueue is a thread-safe queue whose put() blocks when full and whose take() blocks when empty, which is exactly the coordination the producer-consumer pattern needs without any manual wait()/notify(). PriorityBlockingQueue combines that same blocking behavior with priority ordering instead of FIFO – worked through in Deep Dive into Java’s PriorityBlockingQueue.

Going deeper on this section

Executors and futures (Q22-Q27)

Q22. What’s the difference between execute() and submit()?

execute(Runnable) lets an uncaught exception reach the worker thread’s default uncaught-exception handler, which prints a stack trace unprompted. submit(...) instead captures the exception inside the returned Future, and nothing rethrows it unless something calls get() – skip that call and the exact same exception produces zero output anywhere. This exact trap, with the silent-failure transcript, is in ExecutorService Done Right.

Q23. What’s the correct way to shut down an ExecutorService?

Call shutdown() first (stops accepting new work, lets queued and running tasks finish), then awaitTermination(timeout), and only escalate to shutdownNow() if that grace period actually expires. Both branches of that pattern are run for real, not just described, in the executors post.

Q24. What does the AutoCloseable executor change about that pattern?

Since JDK 19, ExecutorService extends AutoCloseable, and its default close() runs essentially that same shutdown-then-await-then-escalate pattern for you – so try-with-resources is a correct one-line replacement, not a shortcut that skips the graceful phase. Verified with real ordering proof in the executors post.

Q25. thenApply vs thenCompose on CompletableFuture – what’s the actual difference?

thenApply transforms a result synchronously with a plain function. Use it when the transformation returns a plain value, like String::toUpperCase. If the transformation itself returns a CompletableFuture, using thenApply nests it into a CompletableFuture<CompletableFuture<T>> – use thenCompose instead to flatten it. Examples and the nesting trap are in Java Concurrency Deep Dive.

Q26. When would you reach for ForkJoinPool over a fixed thread pool?

ForkJoinPool is purpose-built for CPU-bound, recursively-splittable work – it uses work-stealing so idle threads pull sub-tasks from busy threads’ queues, keeping all cores busy even when the work is unbalanced. A fixed pool is the right choice for a flat collection of independent tasks with no recursive structure. Full RecursiveTask example in the concurrency deep dive.

Q27. Why does calling Future.get() inside a CompletableFuture async stage defeat the point?

It blocks a pool thread while waiting – exactly the thread-blocking behavior CompletableFuture‘s callback-chaining model exists to avoid. Under load, blocking pool threads inside async stages can exhaust the pool and stall unrelated work. Discussed with the fix pattern in the concurrency deep dive.

Going deeper on this section

Synchronizers (Q28-Q31)

Q28. CountDownLatch vs CyclicBarrier – what’s the actual difference?

CountDownLatch counts down to zero exactly once and can never be reset – a one-time “go” signal or completion event. CyclicBarrier resets itself automatically the moment every party arrives, so the same instance handles round after round without reallocation. Both implementations of the identical worker pipeline are compared directly in CountDownLatch vs CyclicBarrier vs Phaser vs Semaphore.

Q29. Why does a single CyclicBarrier timeout break it for every other waiter?

A timed-out await() transitions the barrier into a permanently broken state – every other thread’s await(), even ones unrelated to the straggler, immediately throws BrokenBarrierException until something explicitly calls reset(). This is verified directly, with a second unrelated await() throwing immediately, in the synchronizers post.

Q30. What does Semaphore actually solve, and how is it different from a barrier?

A barrier answers “has everyone reached this point?” Semaphore answers a genuinely different question – “how many callers are allowed through at once?” – bounding concurrent access to a resource, independent of any notion of rounds or arrival. Forcing it into a barrier role misrepresents what it’s for; the synchronizers post demonstrates it in its actual role instead.

Q31. When would you pick Phaser over CyclicBarrier?

When the party count needs to change mid-run – a worker can register() into a Phaser that’s already mid-execution, and the very next phase correctly waits for it, something CyclicBarrier can’t do without building an entirely new instance. Also, unlike CyclicBarrier, a Phaser timeout doesn’t permanently break the phaser – a later, fully-arrived phase succeeds normally. Both proven with real dynamic registration and timeout runs in the synchronizers post.

Going deeper on this section

Virtual threads (Q32-Q35)

Q32. What problem do virtual threads solve that a thread pool doesn’t?

A platform thread is expensive (megabyte-scale stack, OS-scheduled), so a pool size is capped and every blocking call ties up a scarce worker for its full duration. Virtual threads are cheap enough to have millions, so a blocking call just unmounts the virtual thread from its carrier instead of occupying anything scarce. Measured directly: 5,000 tasks sleeping 50ms each finished in 121ms on newVirtualThreadPerTaskExecutor() versus 12,556ms on a 20-thread fixed pool, in the executors post.

Q33. What is thread pinning, and did JEP 491 fully fix it?

Pinning is when a virtual thread can’t unmount from its carrier while blocked, because something – a held monitor, or a native stack frame – prevents the JVM from freezing its continuation. JEP 491 (finalized in JDK 24) fixed the synchronized case specifically. It did not fix native-frame pinning (a blocking call made through JNI/FFM while native frames are on the stack) – reproduced with a real JNI library and JFR events in Diagnosing Virtual Thread Pinning in Production, including the finding that -Djdk.tracePinnedThreads=full stays silent for that case on JDK 25.

Q34. Should you still wrap virtual threads in a fixed-size ExecutorService?

No – that reintroduces the exact scarcity virtual threads exist to remove. newVirtualThreadPerTaskExecutor() isn’t a pool with a worker limit; every submitted task gets its own thread immediately, and the measured throughput difference against a bounded pool doing identical blocking work is the whole argument (Q32).

Q35. What’s a common trap when migrating blocking code to virtual threads?

Code that holds a synchronized monitor across a blocking call (fixed by JEP 491 on JDK 24+, but still worth knowing for anyone on JDK 21-23) or that makes a blocking JNI/FFM call (never fixed – see Q33) will pin its carrier regardless of how cheap virtual threads are elsewhere in the same program. The fix demonstrated in the pinning post isn’t eliminating the pinning (architecturally impossible for the native case) but routing it through a small, dedicated executor to contain the blast radius.

Going deeper on this section

Structured concurrency and scoped values (Q36-Q40)

Q36. What problem does StructuredTaskScope solve that raw ExecutorService doesn’t?

With a raw ExecutorService, nothing records that two submitted tasks are logically children of the same operation – if one fails, the other keeps running as a thread leak, and interruption of the calling thread isn’t propagated to either. StructuredTaskScope confines forked subtasks’ lifetimes to a lexical block: they start together, finish together, and a failure in one cancels the rest automatically. The full unstructured-vs-structured comparison is in Structured Concurrency in Java.

Q37. What happens to sibling subtasks when one fails in a structured scope?

With the default joiner, the scope’s join() throws once any subtask fails, and every subtask still running is interrupted as part of the scope closing – none are left running past the failure, unlike the raw-ExecutorService version of the same code. Detailed with the exact five-stage lifecycle (open, fork, join, process, close) in the structured concurrency post.

Q38. StructuredTaskScope vs CompletableFuture – when do you reach for which?

CompletableFuture is for the asynchronous, non-blocking style: composed callbacks, tasks that may outlive the method that created them. StructuredTaskScope is for the blocking, structured style: fork, block once in join(), then handle everything centrally – lifetimes are confined to a block and cancellation is automatic. With cheap virtual threads underneath, “just block” stops being expensive, which is what makes the structured style competitive. Full comparison in the structured concurrency post.

Q39. What are ScopedValues, and which threads actually inherit a binding?

ScopedValue (finalized in JDK 25 via JEP 506) shares immutable data with a method’s direct and indirect callees, with a bounded lifetime and no set() method – unlike ThreadLocal, it can’t leak or be mutated after the fact. The trap: inheritance to a child thread is not automatic for just any thread. Verified directly against all three cases:
ScopedValue.where(REQUEST_ID, "req-100").run(() -> {
    Thread plain = new Thread(() -> REQUEST_ID.isBound());        // false
    try (var scope = StructuredTaskScope.open()) {
        scope.fork(() -> REQUEST_ID.get());                        // "req-100" - inherited
        scope.join();
    }
});
$ java --enable-preview ScopedValueDemo

=== Same-thread binding: visible to direct AND indirect callees ===
handleRequest(): REQUEST_ID.get() = req-42
logSomewhereDeeper() (indirect callee, same thread): REQUEST_ID.get() = req-42

=== Outside any binding: isBound() is false ===
main: REQUEST_ID.isBound() = false

=== A plain `new Thread()` does NOT inherit the binding ===
plain child thread: REQUEST_ID.isBound() = false

=== A StructuredTaskScope.fork()'d thread DOES inherit the binding ===
forked subtask: REQUEST_ID.isBound() = true, REQUEST_ID.get() = req-100
Output: 04-scoped-value-inheritance.txt, source: ScopedValueDemo.java, confirmed by assertion-based tests (Tests run: 9, Failures: 0, output/05). A plain hand-rolled new Thread() reports isBound() = false – only a thread forked from a StructuredTaskScope inherits the binding.

Q40. Is structured concurrency finalized in JDK 25/26?

No. StructuredTaskScope previewed as JEP 505 in JDK 25 and JEP 505 in JDK 26 (as JEP 525, with the Joiner API and allSuccessfulOrThrow() returning a List directly), and finalization is expected around JDK 27. Every example that uses it needs --enable-preview today – the full version history is tabulated in Structured Concurrency in Java. ScopedValue itself, by contrast, is already finalized (Q39).

Going deeper on this section

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.