Concurrency Interview Questions
Concurrency questions separate mid-level from senior Java developers. Know thread lifecycle, synchronization mechanisms, and modern alternatives like virtual threads.
Thread Basics
Q: Ways to create a thread in Java?
// 1. Extend Thread (avoid — limits inheritance)
class MyThread extends Thread {
public void run() { System.out.println("Running"); }
}
// 2. Implement Runnable (preferred)
Thread t = new Thread(() -> System.out.println("Running"));
// 3. Callable + Future (returns result)
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> future = pool.submit(() -> 42);
future.get(); // blocks until result
// 4. Virtual threads (Java 21+)
Thread vThread = Thread.ofVirtual().start(() -> System.out.println("Virtual"));
Q: Thread lifecycle states?
NEW → RUNNABLE → RUNNING → TERMINATED
↕ ↓
BLOCKED WAITING / TIMED_WAITING
- NEW — created, not started
- RUNNABLE — eligible to run
- BLOCKED — waiting for monitor lock
- WAITING — wait(), join() with no timeout
- TIMED_WAITING — sleep(), wait(timeout)
- TERMINATED — run() completed
synchronized Keyword
Q: How does synchronized work?
// Synchronized method — locks on this
public synchronized void increment() {
count++;
}
// Synchronized block — locks on specific object
public void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
}
Only one thread holds the monitor at a time. Other threads block until release.
Q: Problems with synchronized?
- No timeout on lock acquisition — potential deadlock
- No interruptible lock waiting
- No try-lock
- No read/write lock separation
Use java.util.concurrent.locks for advanced control.
volatile Keyword
Q: What does volatile do?
private volatile boolean running = true;
public void stop() {
running = false; // visible to all threads immediately
}
- Guarantees visibility — changes written by one thread are seen by others
- Prevents instruction reordering around volatile access
- Does NOT provide atomicity for compound operations (
count++is not safe with volatile alone)
Use for single-writer flags. For counters, use AtomicInteger.
Atomic Classes
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic, lock-free CAS
counter.compareAndSet(5, 10); // CAS operation
AtomicReference<User> userRef = new AtomicReference<>();
userRef.updateAndGet(u -> new User(u.id(), "Updated"));
Based on Compare-And-Swap (CAS) — no blocking, better under low contention.
Locks
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock(); // ALWAYS in finally
}
// Try lock with timeout — avoids deadlock
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try { /* work */ } finally { lock.unlock(); }
}
// ReadWriteLock — multiple readers OR one writer
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // shared
rwLock.writeLock().lock(); // exclusive
Thread Pools
Q: Why use thread pools instead of creating threads directly?
Creating threads is expensive (1MB stack, OS scheduling). Pools reuse threads:
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 100; i++) {
int taskId = i;
pool.submit(() -> processTask(taskId));
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
| Pool Type | Use Case |
|---|---|
| FixedThreadPool | Steady workload, limit concurrency |
| CachedThreadPool | Short-lived, variable load |
| ScheduledThreadPool | Delayed/periodic tasks |
| SingleThreadExecutor | Sequential task processing |
Production: configure ThreadPoolExecutor explicitly — avoid unbounded CachedThreadPool:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // core pool size
8, // max pool size
60L, TimeUnit.SECONDS, // keep-alive
new LinkedBlockingQueue<>(100), // bounded queue
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
);
CompletableFuture
Q: CompletableFuture vs Future?
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchUserData()) // async supply
.thenApply(user -> user.name()) // transform
.thenCompose(name -> fetchOrders(name)) // chain async
.exceptionally(ex -> "default"); // handle error
// Combine multiple futures
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World");
CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + " " + b);
Non-blocking composition — essential for reactive-style Java.
Virtual Threads (Java 21+)
Q: Virtual threads vs platform threads?
| Platform Thread | Virtual Thread | |
|---|---|---|
| Backed by | OS thread (1:1) | JVM scheduler (M:N) |
| Memory | ~1 MB stack | ~1 KB |
| Count | Thousands max | Millions |
| Blocking I/O | Wastes OS thread | Carrier thread released |
// Create millions of virtual threads for I/O-bound work
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
int id = i;
executor.submit(() -> fetchUrl(id)); // blocking I/O is fine
}
}
Use virtual threads for I/O-bound workloads. CPU-bound work still needs platform threads or fork/join.
Common Concurrency Problems
Q: What is deadlock? How to prevent?
// Deadlock — Thread 1 locks A then B, Thread 2 locks B then A
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
Prevention:
- Lock ordering — always acquire locks in same order
- Lock timeout —
tryLock(timeout) - Reduce lock scope — hold locks for minimum time
- Use concurrent collections instead of synchronized wrappers
Q: What is livelock vs starvation?
- Deadlock — threads blocked forever waiting for each other
- Livelock — threads actively responding to each other but making no progress
- Starvation — thread never gets CPU/lock due to higher-priority threads
Concurrent Collections
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
queue.put(task); // blocks if full
Task t = queue.take(); // blocks if empty
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// Expensive writes, cheap reads — good for read-heavy scenarios
Interview Coding: Print Odd/Even with Two Threads
public class OddEven {
private int count = 1;
private final int max = 20;
private final Object lock = new Object();
public void printOdd() {
synchronized (lock) {
while (count <= max) {
if (count % 2 == 0) lock.wait();
else {
System.out.println("Odd: " + count++);
lock.notify();
}
}
}
}
public void printEven() {
synchronized (lock) {
while (count <= max) {
if (count % 2 != 0) lock.wait();
else {
System.out.println("Even: " + count++);
lock.notify();
}
}
}
}
}
Modern alternative: use Semaphore or BlockingQueue.
Key Takeaways
- Prefer high-level concurrency utilities over raw
wait()/notify() - Use thread pools — never unbounded thread creation
ConcurrentHashMapoverCollections.synchronizedMap()- Virtual threads for I/O-bound; platform threads for CPU-bound
- Always handle
InterruptedExceptionproperly — restore interrupt flag - Test concurrent code — race conditions are non-deterministic
Review Common Questions and Collections to complete interview prep.