Interview Prep
Interview Questions on Multithreading — Synchronized, Volatile, ExecutorService, Deadlock & Producer-Consumer
Java multithreading is tested in every senior Java interview. It separates junior developers from senior ones. Every product company and senior service company role in India tests it — from Flipkart to Infosys Digital.

Multithreading is the most feared Java interview topic — and the most rewarding to master.
Multithreading in Java Interviews
Multithreading is the most feared Java interview topic. It separates junior from senior developers. Every product company — Google, Amazon, Flipkart, Razorpay — and every senior service company role at TCS Digital, Infosys Power Programmer, and Wipro Turbo tests it.
The questions follow a predictable pattern: thread basics for 2-3 year experience, synchronization and concurrent collections for 4-5 years, and advanced concurrency patterns for 6+ years. If you can explain thread lifecycle, write synchronized code, and solve a producer-consumer problem on a whiteboard, you clear the round.
This guide covers 10 actual multithreading questions asked in Indian Java interviews — with code examples, diagrams, and the depth interviewers expect at each experience level.
“Explain the difference between synchronized and volatile” — this single question has decided more senior Java offers than any other.
Thread Basics
Q1: What is the difference between a process and a thread?
Process vs Thread: Process: Thread: ┌─────────────────────┐ ┌─────────────────────┐ │ Own memory space │ │ Shared memory (heap)│ │ Own heap & stack │ │ Own stack only │ │ Independent │ │ Lightweight │ │ IPC needed to talk │ │ Direct communication│ │ Expensive to create │ │ Cheap to create │ └─────────────────────┘ └─────────────────────┘ Process = independent program with its own memory. Thread = lightweight unit within a process sharing memory. Multiple threads in one process: ┌──────────────────────────────────┐ │ Process (JVM) │ │ ┌────────┐ ┌────────┐ ┌──────┐ │ │ │Thread 1│ │Thread 2│ │Thread│ │ │ │ Stack │ │ Stack │ │Stack │ │ │ └────────┘ └────────┘ └──────┘ │ │ ┌──────────────────────────────┐│ │ │ Shared Heap Memory ││ │ └──────────────────────────────┘│ └──────────────────────────────────┘ Key: Threads share heap but have separate stacks. This is why multithreading needs synchronization.
Q2: What are the states in a thread lifecycle?
Thread Lifecycle (6 states in Java):
NEW ──start()──→ RUNNABLE ──scheduler──→ RUNNING
↑ │
│ ├─sleep()──→ TIMED_WAITING
│ ├─wait()───→ WAITING
│ ├─lock──────→ BLOCKED
│ │
←──notify()/timeout──────┘
│
TERMINATED
States:
NEW → Thread created, not yet started
RUNNABLE → Ready to run, waiting for CPU
RUNNING → Currently executing
BLOCKED → Waiting to acquire a lock
WAITING → Waiting indefinitely (wait(), join())
TIMED_WAITING → Waiting with timeout (sleep(), wait(ms))
TERMINATED → Execution complete
// Creating and starting a thread:
Thread t = new Thread(() -> {
System.out.println("Running: " + Thread.currentThread().getName());
});
System.out.println(t.getState()); // NEW
t.start();
// State is now RUNNABLE → RUNNING
t.join(); // Wait for thread to finish
System.out.println(t.getState()); // TERMINATEDQ3: What is the difference between Runnable and Callable?
Runnable vs Callable:
Runnable:
- void run() → no return value
- Cannot throw checked exceptions
- Available since Java 1.0
Callable:
- V call() → returns a value via Future
- Can throw checked exceptions
- Available since Java 1.5
// Runnable — no return value
Runnable task = () -> {
System.out.println("Processing...");
};
new Thread(task).start();
// Callable — returns a value
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
// future.get() blocks until result is ready
Integer result = future.get(); // 42
System.out.println("Result: " + result);
executor.shutdown();
// Use Runnable for fire-and-forget tasks
// Use Callable when you need a result or exception handlingSynchronization
Q4: What is synchronized? How does it work?
synchronized = intrinsic lock (monitor) mechanism.
Only one thread can hold the lock at a time.
// 1. Synchronized method — lock on 'this' object
public synchronized void increment() {
count++; // only one thread at a time
}
// 2. Synchronized block — lock on specific object
public void increment() {
synchronized (this) {
count++;
}
}
// 3. Static synchronized — lock on Class object
public static synchronized void increment() {
count++; // lock on MyClass.class
}
// Equivalent to:
public static void increment() {
synchronized (MyClass.class) {
count++;
}
}
// Key points:
// - synchronized method = coarse-grained (locks entire method)
// - synchronized block = fine-grained (locks only critical section)
// - Static synchronized = class-level lock (shared across instances)
// - Every object in Java has an intrinsic lock
// - Reentrant: same thread can acquire same lock multiple timesQ5: What is volatile? How is it different from synchronized?
volatile vs synchronized:
volatile:
- Guarantees VISIBILITY (reads from main memory, not CPU cache)
- Does NOT guarantee atomicity
- No locking, no blocking
- Use for: flags, booleans, simple state
synchronized:
- Guarantees both VISIBILITY and ATOMICITY
- Uses locking (can cause blocking)
- Use for: compound operations (check-then-act, read-modify-write)
// volatile example — flag to stop a thread
class Worker implements Runnable {
private volatile boolean running = true;
public void run() {
while (running) { // reads from main memory every time
// do work
}
}
public void stop() {
running = false; // visible to all threads immediately
}
}
// Why volatile is NOT enough for count++:
// count++ is actually 3 operations:
// 1. Read count
// 2. Increment
// 3. Write count
// Two threads can read the same value before either writes.
// volatile ensures visibility but NOT atomicity of these 3 steps.
// For atomic increment, use:
// - synchronized
// - AtomicInteger.incrementAndGet()
// - LockQ6: What is a deadlock? How do you prevent it?
Deadlock = two threads waiting for each other's locks.
Neither can proceed. Program hangs forever.
// Classic deadlock example:
Object lockA = new Object();
Object lockB = new Object();
// Thread 1:
synchronized (lockA) { // holds lockA
Thread.sleep(100);
synchronized (lockB) { // waits for lockB (held by Thread 2)
// never reached
}
}
// Thread 2:
synchronized (lockB) { // holds lockB
Thread.sleep(100);
synchronized (lockA) { // waits for lockA (held by Thread 1)
// never reached
}
}
// Deadlock diagram:
// Thread 1 ──holds──→ lockA ←──waits── Thread 2
// Thread 1 ──waits──→ lockB ←──holds── Thread 2
// Prevention strategies:
// 1. Lock ordering — always acquire locks in the same order
synchronized (lockA) {
synchronized (lockB) { /* safe */ }
}
// Both threads lock A first, then B. No circular wait.
// 2. tryLock with timeout (ReentrantLock)
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try { /* work */ } finally { lock.unlock(); }
}
// 3. Avoid nested locks when possible
// 4. Detection: jstack <pid> → shows thread dump with deadlocksConcurrency API
Q7: What is ExecutorService? What are the thread pool types?
ExecutorService = framework for managing thread pools.
Why? Creating threads manually is expensive and uncontrolled.
// Thread pool types:
// 1. FixedThreadPool — fixed N threads
ExecutorService pool = Executors.newFixedThreadPool(5);
// 5 threads. Extra tasks wait in queue.
// Use for: known, steady workload
// 2. CachedThreadPool — creates threads as needed
ExecutorService pool = Executors.newCachedThreadPool();
// Creates new threads on demand, reuses idle ones (60s timeout)
// Use for: many short-lived tasks
// 3. SingleThreadExecutor — 1 thread
ExecutorService pool = Executors.newSingleThreadExecutor();
// Tasks execute sequentially, one at a time
// Use for: sequential task processing
// 4. ScheduledThreadPool — delayed/periodic tasks
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
pool.scheduleAtFixedRate(() -> {
System.out.println("Runs every 5 seconds");
}, 0, 5, TimeUnit.SECONDS);
// Usage pattern:
ExecutorService executor = Executors.newFixedThreadPool(5);
Future<String> future = executor.submit(() -> {
return "Task completed";
});
String result = future.get(); // blocks until done
executor.shutdown(); // graceful shutdown
// Why thread pools > manual threads:
// - Reuses threads (no creation overhead)
// - Controls max concurrent threads
// - Provides task queue management
// - Graceful shutdown supportQ8: What are concurrent collections?
Concurrent collections = thread-safe collections
without locking the entire structure.
// 1. ConcurrentHashMap
// Segment-level locking (Java 8+: node-level CAS)
// Thread-safe without locking entire map
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.computeIfAbsent("key2", k -> 2);
// Use when: multiple threads read/write a shared map
// 2. CopyOnWriteArrayList
// Creates a new copy on every write
// Reads are lock-free (fast)
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("item"); // copies entire array
// Use when: read-heavy, write-rare (event listeners, config)
// 3. BlockingQueue
// Producer-consumer pattern
// put() blocks if full, take() blocks if empty
BlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
queue.put("task"); // blocks if queue is full
queue.take(); // blocks if queue is empty
// Use when: producer-consumer, task queues
// 4. ConcurrentLinkedQueue
// Lock-free queue using CAS operations
// Non-blocking (offer/poll return immediately)
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
// Use when: high-throughput, non-blocking scenarios
// When to use each:
// HashMap → ConcurrentHashMap (general purpose)
// ArrayList → CopyOnWriteArrayList (read-heavy)
// LinkedList → BlockingQueue (producer-consumer)
// Queue → ConcurrentLinkedQueue (lock-free)
Concurrency questions separate mid-level from senior Java developers in every interview.
Practical Scenarios
Q9: Implement the Producer-Consumer pattern.
// Producer-Consumer using BlockingQueue
// BlockingQueue handles all synchronization automatically.
import java.util.concurrent.*;
public class ProducerConsumer {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);
// Producer thread
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
System.out.println("Producing: " + i);
queue.put(i); // blocks if queue is full
Thread.sleep(100);
}
queue.put(-1); // poison pill to signal stop
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Consumer thread
Thread consumer = new Thread(() -> {
try {
while (true) {
Integer item = queue.take(); // blocks if empty
if (item == -1) break; // poison pill
System.out.println("Consuming: " + item);
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
// Why BlockingQueue > wait/notify:
// - No manual synchronization needed
// - No spurious wakeup handling
// - Built-in capacity management
// - Cleaner, less error-prone code
// - Multiple producer/consumer support out of the boxQ10: What is the difference between wait/notify and Lock/Condition?
wait/notify vs Lock/Condition:
wait/notify:
- Tied to synchronized keyword
- Object-level (one wait set per object)
- No fairness control
- Cannot interrupt a waiting thread easily
Lock/Condition:
- Uses ReentrantLock
- Multiple conditions per lock
- tryLock with timeout
- Fairness option (FIFO ordering)
// wait/notify example:
synchronized (lock) {
while (!condition) {
lock.wait(); // releases lock, waits
}
// do work
lock.notifyAll(); // wake up waiting threads
}
// Lock/Condition example:
ReentrantLock lock = new ReentrantLock(true); // fair lock
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // wait for items
}
// consume item
notFull.signal(); // signal producers
} finally {
lock.unlock();
}
// When to use each:
// synchronized + wait/notify → simple scenarios
// Lock/Condition → complex scenarios:
// - Multiple conditions (notEmpty, notFull)
// - tryLock with timeout
// - Fairness requirement
// - Need to interrupt waiting threadsHow to Prepare
Multithreading — Priority by Experience
2-3 Years
- • Thread lifecycle & states
- • synchronized keyword
- • volatile keyword
- • Runnable vs Callable
- • Basic thread creation
4-5 Years
- • ExecutorService & thread pools
- • Concurrent collections
- • Deadlock detection & prevention
- • Producer-consumer pattern
- • ReentrantLock & Condition
6+ Years
- • Fork/Join framework
- • CompletableFuture
- • Lock-free algorithms (CAS)
- • Thread dump analysis
- • Performance tuning