Concurrency Interview Questions
Java concurrency is one of the most challenging and heavily tested topics in technical interviews. This page walks through the most important questions — from classic threading basics to modern java.util.concurrent APIs — with clear, accurate answers and illustrative code.
Fundamentals
Q1: What is the difference between a process and a thread?
A process is an independent program instance with its own memory space. A thread is a lightweight unit of execution that lives inside a process and shares the process’s heap and static memory with other threads in the same process, while each thread owns its own stack and program counter.
| Aspect | Process | Thread |
|---|---|---|
| Memory | Isolated address space | Shared heap with siblings |
| Creation cost | Heavy (OS fork/exec) | Light (stack allocation only) |
| Communication | IPC (pipes, sockets…) | Shared variables (needs synchronization) |
| Failure isolation | Crash stays contained | One thread can crash the JVM |
Q2: What are the thread states in Java?
Java threads cycle through six states defined in Thread.State:
public class ThreadStates {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try { Thread.sleep(500); } catch (InterruptedException e) {}
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE
t.join();
System.out.println(t.getState()); // TERMINATED
}
}
The full lifecycle is covered in detail on the Thread Life Cycle page.
Q3: What is the difference between run() and start()?
Calling run() directly executes the thread body on the calling thread — no new thread is created. Only start() tells the JVM to spawn a new OS thread and then invoke run() on it. This is a common beginner trap; see run() vs start() for more.
Synchronization & Visibility
Q4: What does synchronized mean and what does it guarantee?
The synchronized keyword acquires a monitor lock (also called an intrinsic lock) on an object before executing the protected block, and releases it on exit — even if an exception is thrown. It provides two guarantees:
- Mutual exclusion — only one thread holds the lock at a time.
- Memory visibility — changes made inside a synchronized block are visible to any thread that subsequently acquires the same lock.
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // read-modify-write is now atomic
}
public synchronized int get() {
return count;
}
}
Warning:
synchronizedon different objects does NOT protect the same data. Always synchronize on the same lock to guard the same resource.
Q5: What is volatile and when should you use it?
volatile guarantees visibility but not atomicity. Every write to a volatile variable is flushed to main memory immediately, and every read fetches the latest value from main memory, bypassing CPU caches.
public class StopFlag {
private volatile boolean running = true;
public void stop() { running = false; }
public void run() {
while (running) {
// do work
}
System.out.println("Stopped.");
}
}
Without volatile, the worker thread might cache running in a register and loop forever. Use volatile for simple flags or state variables where only a single write/read operation is needed. For compound actions (check-then-act, increment), use synchronized or AtomicInteger instead.
See the volatile Keyword page and Java Memory Model for deeper coverage.
Q6: What is the difference between synchronized and ReentrantLock?
Both achieve mutual exclusion, but ReentrantLock gives you more control:
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Reentrant? | Yes | Yes |
| Try-lock (non-blocking) | No | tryLock() |
| Timed lock attempt | No | tryLock(time, unit) |
| Interruptible lock wait | No | lockInterruptibly() |
| Fairness policy | No | new ReentrantLock(true) |
| Multiple conditions | No | lock.newCondition() |
| Must release manually | N/A | Yes — use finally |
import java.util.concurrent.locks.ReentrantLock;
public class SafeCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // always in finally
}
}
}
Tip: Prefer
synchronizedfor simple cases — it’s less error-prone. Reach forReentrantLockwhen you needtryLock, timed waits, or multiple condition variables.
Full details are on the ReentrantLock & Monitors page.
Deadlock, Livelock & Starvation
Q7: What is a deadlock? How do you prevent it?
A deadlock occurs when two or more threads each hold a lock the other needs, forming a cycle where none can proceed.
// Classic deadlock scenario
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
synchronized (lockB) { /* work */ }
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) { // acquires B first — opposite order!
synchronized (lockA) { /* work */ }
}
});
Prevention strategies:
- Lock ordering — always acquire locks in the same global order.
tryLockwith timeout — back off and retry if you can’t acquire all locks.- Lock-free data structures — use
java.util.concurrent.atomicclasses. - Minimize lock scope — hold locks for the shortest time possible.
See Deadlock for a full walkthrough.
Q8: What is the difference between deadlock, livelock, and starvation?
- Deadlock — threads are blocked forever waiting on each other.
- Livelock — threads are actively running but keep reacting to each other and making no real progress (like two people stepping aside for each other in a hallway, indefinitely).
- Starvation — a thread is perpetually denied CPU time or a lock because higher-priority threads keep taking it.
The Java Memory Model
Q9: What is the Java Memory Model (JMM)?
The JMM defines the rules under which one thread’s writes become visible to another thread’s reads. Without it, the JVM and CPU are free to reorder instructions and cache values — which is great for performance but can break multi-threaded code.
The key concept is a happens-before relationship. If action A happens-before action B, then B is guaranteed to see A’s effects. Happens-before edges are created by:
- A thread calling
start()on another thread. - A thread’s
join()returning. - Releasing a monitor (
synchronizedexit) and a subsequent acquisition of the same monitor. - Writing to a
volatilevariable and a subsequent read of that same variable.
See the full Java Memory Model page for more.
Q10: What is a race condition?
A race condition occurs when the correctness of a program depends on the relative timing of thread execution — and that timing is not controlled. The classic example is a non-atomic check-then-act:
// UNSAFE — two threads can both pass the if-check before either decrements
if (count > 0) {
count--;
}
// SAFE — use AtomicInteger or synchronize
AtomicInteger atomicCount = new AtomicInteger(10);
if (atomicCount.getAndDecrement() > 0) {
// safely consumed one unit
}
java.util.concurrent Utilities
Q11: What is the Executor framework and why use it?
Creating raw threads is expensive and hard to manage at scale. The Thread Pools & Executors framework from java.util.concurrent decouples task submission from execution mechanics.
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
final int taskId = i;
pool.submit(() -> System.out.println("Task " + taskId + " on " + Thread.currentThread().getName()));
}
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
Q12: What is Callable vs Runnable? What is Future?
Runnable.run()returnsvoidand cannot throw checked exceptions.Callable<V>.call()returns a result of typeVand can throw checked exceptions.Future<V>represents the eventual result of aCallable. Callfuture.get()to block until the result is ready.
ExecutorService exec = Executors.newSingleThreadExecutor();
Future<Integer> future = exec.submit(() -> {
Thread.sleep(200);
return 42;
});
System.out.println("Result: " + future.get()); // blocks until done
exec.shutdown();
Output:
Result: 42
Details on Callable & Future.
Q13: What are atomic variables?
java.util.concurrent.atomic provides lock-free thread-safe operations on single variables using CPU-level Compare-And-Swap (CAS) instructions.
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic, no lock needed
counter.compareAndSet(1, 2); // CAS: set to 2 only if current value is 1
Common classes: AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference<V>.
Q14: What is CountDownLatch and when would you use it?
CountDownLatch lets one or more threads wait until a set of operations in other threads completes. You initialize it with a count; worker threads call countDown(), and the waiting thread calls await().
import java.util.concurrent.*;
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " done");
latch.countDown();
}).start();
}
latch.await(); // main thread waits for all 3
System.out.println("All workers finished.");
Note: A
CountDownLatchcannot be reset. UseCyclicBarrierif you need a reusable barrier that resets after each cycle.
Q15: What is ConcurrentHashMap and how is it different from Hashtable?
Both are thread-safe maps, but they differ significantly in performance:
| Feature | Hashtable | ConcurrentHashMap |
|---|---|---|
| Lock scope | Entire map | Per-bucket (segment-level in Java 7, node-level in Java 8+) |
null keys/values | Not allowed | Not allowed |
| Read operations | Locked | Lock-free (read without locking) |
| Performance | Bottlenecks under contention | Scales well with many threads |
| Iterator | Fail-fast | Weakly consistent (does not throw ConcurrentModificationException) |
See Concurrent Collections for the full picture.
Under the Hood
How JVM Synchronization Works
When a thread enters a synchronized block, the JVM executes a monitorenter bytecode instruction, attempting to acquire the object’s monitor. If another thread holds it, the requesting thread is parked (moved to the monitor’s wait set) and de-scheduled by the OS.
In modern JVMs (HotSpot), monitors are optimized in three tiers:
- Biased locking — if only one thread ever accesses the object, the lock is “biased” toward it and future re-acquisitions cost nearly zero (removed in Java 21 as it added JVM complexity).
- Thin lock (stack lock) — if a second thread competes, a CAS on the object header is used — still very fast.
- Fat lock (inflated monitor) — if contention persists, a full OS mutex is used.
This tiering means that lightly-contended synchronized code is far faster than its reputation suggests. Benchmark before switching to ReentrantLock for performance reasons alone.
Virtual Threads and Concurrency (Java 21)
Virtual Threads, introduced as a preview in Java 19 and made stable in Java 21, are lightweight threads managed by the JVM rather than the OS. They allow you to write blocking code that scales like async code — you can have millions of virtual threads without exhausting OS thread limits.
// Java 21: spawn a virtual thread
Thread.ofVirtual().start(() -> System.out.println("Virtual thread running"));
For CPU-bound work, platform threads + thread pools remain the right tool. Virtual threads shine for I/O-heavy workloads.
Quick-Reference: Most Common Pitfalls
- Forgetting
volatileon a flag — the reading thread may never see the update. - Using
++on a shared int without synchronization — not atomic. - Calling
wait()/notify()outside asynchronizedblock — throwsIllegalMonitorStateException. - Not calling
unlock()in afinallyblock — leaves locks permanently held on exceptions. - Calling
start()twice — throwsIllegalThreadStateException; see Starting a Thread Twice.
Related Topics
- Multithreading — foundational concepts for all concurrency topics
- Synchronization — deep dive into
synchronizedand monitors - Java Memory Model — happens-before, visibility, and instruction reordering
- ReentrantLock & Monitors — explicit locking with
java.util.concurrent.locks - Concurrent Collections —
ConcurrentHashMap,CopyOnWriteArrayList, and more - Virtual Threads (Project Loom) — lightweight threads in Java 21