Java Multithreading and Concurrency Interview Questions: Deep Dive
Concurrency is where Java interviews separate seniors from everyone else. The questions look simple — "what does volatile do?" — but each one has a trapdoor. This deep dive gives you the precise answer plus the follow-up hiding underneath.
1. What does volatile actually guarantee? (The #1 trap)
Answer: volatile guarantees visibility (writes are immediately visible to other threads) and ordering (no instruction reordering around the access) — but NOT atomicity. This fails:
volatile int counter = 0; counter++; // read-modify-write: NOT atomic, even with volatile!
Use volatile for flags (volatile boolean running = true); use AtomicInteger or synchronization for read-modify-write. Saying "volatile makes it thread-safe" is the fastest way to fail the round.
2. synchronized vs ReentrantLock?
synchronized (lock) { /* ... */ } // block-structured, auto-release
lock.lock();
try { /* ... */ } finally { lock.unlock(); } // must release manually
Answer: synchronized is simpler and the JVM optimizes it aggressively (biased/lock elision). ReentrantLock buys you: timed/try-lock (tryLock(1, SECONDS)), interruptible lock acquisition, fairness policy, and multiple Conditions. Rule of thumb: synchronized until you need one of those four.
3. Explain deadlock — and the four conditions
// classic: opposite lock ordering
Thread A: synchronized (lock1) { synchronized (lock2) { ... } }
Thread B: synchronized (lock2) { synchronized (lock1) { ... } }
Answer: deadlock needs all four Coffman conditions: mutual exclusion, hold-and-wait, no preemption, circular wait. You break it by breaking circular wait — impose a global lock ordering (always acquire lock1 before lock2). Follow-ups: detect via jstack thread dumps or ThreadMXBean.findDeadlockedThreads(); prevent with tryLock timeouts.
4. How do you size a thread pool?
Executors.newFixedThreadPool(nThreads); // CPU-bound: nThreads ≈ cores Executors.newCachedThreadPool(); // short-lived tasks (unbounded — dangerous) Executors.newVirtualThreadPerTaskExecutor(); // Java 21+: blocking I/O at scale
Answer: CPU-bound → threads ≈ CPU cores (more threads just context-switch). I/O-bound → threads ≈ cores × (1 + wait/compute ratio). And never create threads in a loop — unbounded creation is how production falls over; a bounded pool with a rejection policy (AbortPolicy, CallerRunsPolicy) is the professional answer.
5. How does ConcurrentHashMap achieve thread safety?
Answer: lock striping — the map is divided into segments/buckets with fine-grained locking (modern JDK: CAS + synchronized on individual bins), so 16 threads writing different keys rarely contend. Reads are mostly lock-free via volatile table entries. Contrast: Collections.synchronizedMap locks the whole map per operation, and Hashtable is its legacy twin. Bonus: computeIfAbsent is atomic — the canonical cache pattern.
6. wait/notify vs Condition?
Answer: wait()/notify() work with synchronized and give you one wait-set per lock. Condition (from a Lock) gives you multiple wait-sets per lock — e.g. a bounded buffer with separate notFull and notEmpty conditions, so producers and consumers wake only the right waiters. Always call wait() in a while loop re-checking the condition (spurious wakeups are real).
7. CompletableFuture in 60 seconds
CompletableFuture.supplyAsync(() -> fetchUser(id))
.thenCombine(CompletableFuture.supplyAsync(() -> fetchOrders(id)),
(user, orders) -> new Profile(user, orders))
.exceptionally(ex -> Profile.empty()) // never leave a future hanging
.join();
Answer: monadic async composition — thenApply (sync transform), thenCompose (async flatMap), thenCombine/allOf (joining), exceptionally (recovery). Default pool is the common ForkJoinPool — pass your own executor for I/O work so you don't starve it. The interview follow-up: "Future vs CompletableFuture?" — Future.get() blocks with no composition; CompletableFuture chains without blocking.
8. What is ThreadLocal, and what's the catch?
Answer: per-thread storage — each thread sees its own value (classic uses: SimpleDateFormat instances, request contexts). The catch: in thread pools, threads are reused, so a stale ThreadLocal leaks data across tasks (and memory, if never removed) — always remove() in a finally. In Java 21+, prefer scoped values for immutable inherited context.
Quick-fire revision table
| Question | One-line answer |
|---|---|
| volatile | Visibility + ordering, NOT atomicity |
| synchronized vs Lock | Simple vs timed/try/interruptible/fair |
| Deadlock fix | Global lock ordering breaks circular wait |
| Pool sizing | CPU-bound ≈ cores; I/O-bound ≈ cores × (1 + wait/compute) |
| ConcurrentHashMap | Fine-grained bin locking + lock-free reads |
| CompletableFuture | Non-blocking async composition with recovery |
In this series
- Java 17 to 21 Interview Questions: Records, Pattern Matching, Virtual Threads — records, pattern matching, virtual threads.
- Spring Boot Interview Questions and Answers (2026 Edition) — auto-configuration to actuators.
- Java Multithreading and Concurrency Interview Questions: Deep Dive (this post) — volatile, locks, deadlocks, pools.
- Microservices Interview Questions for Java Developers — decomposition, resilience, sagas.
More: Java Interview Prep hub — the full collection of Java interview questions on JavaMakeUse.
Comments
Post a Comment