- Process vs Thread
- Thread Lifecycle
- Creating Threads
- Synchronization Basics
- Locks vs Synchronized
- Deadlock
- wait(), notify(), notifyAll()
- volatile Keyword
- Atomic Variables
- Thread Pools & Executors
- Producer-Consumer Problem
- Mutex vs Semaphore
- Monitor
- Thread Safety Techniques
- Concurrent Collections
- Happens-Before Relationship
- Common Interview Questions
- Own memory space (separate heap, stack, code segment)
- Heavyweight - slow context switching (requires OS intervention)
- IPC required for communication (pipes, sockets, shared memory)
- Isolation - crash in one process doesn't affect others
- Resource intensive - more memory overhead
- Shares heap with other threads in the same process
- Own stack & program counter (registers)
- Lightweight - fast context switching
- Direct communication through shared memory
- Efficient - lower memory overhead
Interview Line: "Threads improve throughput by sharing memory within a process, but require synchronization mechanisms to avoid race conditions and ensure data consistency."
Use Cases:
- Processes: Isolation needed (browsers use separate processes per tab)
- Threads: Performance-critical, frequent communication (web servers handling requests)
NEW → RUNNABLE → RUNNING → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
- NEW - Thread created but not started
- RUNNABLE - Ready to run, waiting for CPU
- RUNNING - Currently executing
- BLOCKED - Waiting for monitor lock
- WAITING - Waiting indefinitely for another thread
- TIMED_WAITING - Waiting for specified time
- TERMINATED - Execution completed
| Method | State | Releases Lock? |
|---|---|---|
sleep() |
TIMED_WAITING | ❌ NO |
wait() |
WAITING | ✅ YES |
join() |
WAITING | ✅ YES |
yield() |
RUNNABLE | N/A |
sleep() does NOT release locks, wait() does!
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
// Usage
new MyThread().start();class MyTask implements Runnable {
public void run() {
System.out.println("Task running");
}
}
// Usage
new Thread(new MyTask()).start();Callable<Integer> task = () -> {
return 42;
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
Integer result = future.get(); // blockingExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
// task logic
});
executor.shutdown();Why Runnable > Thread:
- Allows class to extend other classes
- Separation of task logic from execution
- Better OOP design
- Compatible with thread pools
- Race Conditions - Multiple threads accessing shared data
- Visibility Issues - Changes not visible across threads
- Atomicity - Operations not atomic by default
// Method level
public synchronized void increment() {
count++;
}
// Block level
synchronized(lock) {
// critical section
count++;
}What it does:
- Provides mutual exclusion (one thread at a time)
- Establishes happens-before relationship
- Ensures visibility of changes
- Don't synchronize on mutable objects (String, Integer)
- Avoid synchronizing on
thisin public APIs - Excessive synchronization kills performance
- Implicit lock acquisition/release
- Automatic release on exception
- Simple to use
- Block-scoped only
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock(); // must be in finally
}Advanced Features:
// Try-lock with timeout
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// critical section
} finally {
lock.unlock();
}
}
// Fairness
ReentrantLock fairLock = new ReentrantLock(true);
// Check if held
lock.isHeldByCurrentThread();
// Interruptible
lock.lockInterruptibly();| Feature | synchronized | ReentrantLock |
|---|---|---|
| Try-lock | ❌ | ✅ |
| Timeout | ❌ | ✅ |
| Fairness | ❌ | ✅ |
| Interruptible | ❌ | ✅ |
| Complexity | Simple | More control |
Interview Line: "ReentrantLock offers more flexibility than synchronized but requires explicit unlock management. Use synchronized for simple cases, ReentrantLock when you need try-lock, timeouts, or fairness."
All must be present for deadlock:
- Mutual Exclusion - Resources cannot be shared
- Hold and Wait - Thread holds resources while waiting for others
- No Preemption - Resources cannot be forcibly taken
- Circular Wait - Circular chain of threads waiting for resources
// Thread 1
synchronized(lockA) {
synchronized(lockB) {
// work
}
}
// Thread 2
synchronized(lockB) {
synchronized(lockA) {
// work - DEADLOCK!
}
}1. Lock Ordering ✅
// Always acquire locks in same order
synchronized(lockA) {
synchronized(lockB) {
// safe
}
}2. Timeout with tryLock
if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// work
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}3. Avoid Nested Locks
4. Use Deadlock Detection Tools
- JConsole, VisualVM
- Thread dumps
- Must be called inside synchronized block
- Called on the monitor object
wait()releases the locknotify()does NOT release lock immediately
synchronized(lock) {
while (!condition) {
lock.wait(); // releases lock, waits
}
// condition met, proceed
}
synchronized(lock) {
condition = true;
lock.notifyAll(); // wakes all waiting threads
}| Method | Releases Lock? | Wakes Threads | Use Case |
|---|---|---|---|
wait() |
✅ YES | N/A | Wait for condition |
notify() |
Eventually | One thread | Single waiter |
notifyAll() |
Eventually | All threads | Multiple waiters |
- Always use
whileloop, notif(spurious wakeups) - Prefer
notifyAll()overnotify()(safer) - Must own the monitor (be in synchronized block)
✅ Visibility - Changes visible to all threads immediately
✅ Ordering - Prevents instruction reordering
❌ NOT Atomicity - count++ is still unsafe
private volatile boolean running = true;
// Thread 1
public void stop() {
running = false; // visible immediately
}
// Thread 2
public void run() {
while (running) {
// work
}
}- Flags and status variables
- Double-checked locking
- Single writer, multiple readers
volatile int count = 0;
count++; // NOT thread-safe! (read-modify-write)Interview Line: "volatile ensures visibility across threads but doesn't provide atomicity. Use it for flags, use AtomicInteger for counters."
AtomicInteger,AtomicLong,AtomicBooleanAtomicReference- CAS-based (Compare-And-Swap) - lock-free
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // atomic i++
count.getAndIncrement(); // atomic ++i
count.addAndGet(5); // atomic += 5
count.compareAndSet(10, 20); // CAS// Pseudocode
boolean compareAndSet(int expected, int update) {
if (value == expected) {
value = update;
return true;
}
return false;
}// NOT ATOMIC
if (count.get() == 0) {
count.incrementAndGet(); // race condition here
}- Avoid thread creation overhead
- Control concurrency (limit threads)
- Prevent resource exhaustion
- Reuse threads
1. FixedThreadPool
ExecutorService executor = Executors.newFixedThreadPool(10);
// Fixed number of threads, unbounded queue2. CachedThreadPool
ExecutorService executor = Executors.newCachedThreadPool();
// Creates threads as needed, reuses idle threads3. SingleThreadExecutor
ExecutorService executor = Executors.newSingleThreadExecutor();
// Single worker thread, sequential execution4. ScheduledThreadPool
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.schedule(task, 10, TimeUnit.SECONDS);
executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);ExecutorService executor = Executors.newFixedThreadPool(10);
try {
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
// task
});
}
} finally {
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
}Interview Line: "Thread pools decouple task submission from execution strategy, providing better resource management and scalability."
CPU-bound: threads = CPU cores + 1
I/O-bound: threads = CPU cores * (1 + wait/compute ratio)
Producers create data, consumers process it. Need synchronization to prevent:
- Producers overwriting data
- Consumers processing same data twice
- Buffer overflow/underflow
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // buffer full
}
queue.add(item);
notifyAll(); // wake consumers
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // buffer empty
}
int item = queue.remove();
notifyAll(); // wake producers
return item;
}
}BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// Producer
queue.put(item); // blocks if full
// Consumer
int item = queue.take(); // blocks if emptyBlockingQueue Implementations:
ArrayBlockingQueue- bounded, array-basedLinkedBlockingQueue- optionally bounded, linked-nodePriorityBlockingQueue- unbounded, priority-orderedSynchronousQueue- no capacity, direct handoff
- Binary lock (locked/unlocked)
- Ownership - same thread must acquire and release
- Purpose: Protect critical section
ReentrantLock mutex = new ReentrantLock();
mutex.lock();
try {
// critical section
} finally {
mutex.unlock();
}- Counter-based (n permits)
- No ownership - any thread can release
- Purpose: Control access to resource pool
Semaphore semaphore = new Semaphore(3); // 3 permits
semaphore.acquire(); // decrements counter
try {
// access resource
} finally {
semaphore.release(); // increments counter
}| Feature | Mutex | Semaphore |
|---|---|---|
| Type | Binary (0/1) | Counter (0 to n) |
| Ownership | Required | Not required |
| Use Case | Critical section | Resource pool |
| Example | Lock | Connection pool |
While binary semaphore (count=1) seems similar to mutex:
- Mutex has ownership semantics
- Semaphore can be released by any thread
- Use mutex for critical sections, semaphore for signaling
// Allow max 5 concurrent database connections
Semaphore dbConnections = new Semaphore(5);
public void queryDatabase() throws InterruptedException {
dbConnections.acquire();
try {
// execute query
} finally {
dbConnections.release();
}
}A high-level synchronization construct that combines:
- Mutex (mutual exclusion)
- Condition variables (wait/notify)
- Encapsulation of shared data
Every Java object is a monitor:
synchronizedprovides mutexwait()/notify()provides condition variables
class BoundedBuffer {
private Queue<Integer> queue = new LinkedList<>();
private int capacity;
// Monitor: encapsulates data + synchronization
public synchronized void put(int item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // condition: not full
}
queue.add(item);
notifyAll();
}
public synchronized int get() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // condition: not empty
}
int item = queue.remove();
notifyAll();
return item;
}
}- Mutex: Low-level lock
- Monitor: High-level construct (mutex + condition variables + data)
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// No setters, thread-safe by design
}public synchronized void increment() {
count++;
}// Each thread has own instance
ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd")
);AtomicInteger count = new AtomicInteger();ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();Best Practice: Prefer immutability > thread confinement > concurrent collections > synchronization
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
map.putIfAbsent("key", 2); // atomic
map.compute("key", (k, v) -> v == null ? 1 : v + 1); // atomicHow it works:
- Segmented locking (pre-Java 8)
- CAS + synchronized (Java 8+)
- Fine-grained locking for better concurrency
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();When to use: Read-heavy, write-rare scenarios
- Writes: Create copy of array (expensive)
- Reads: Lock-free, very fast
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);
queue.put(item); // blocks if full
queue.take(); // blocks if empty
queue.offer(item); // non-blocking, returns false if full
queue.poll(); // non-blocking, returns null if emptyList<String> list = Collections.synchronizedList(new ArrayList<>());
// Still need external synchronization for compound operations
synchronized(list) {
if (list.isEmpty()) {
list.add("item");
}
}Action A happens-before action B means:
- Memory writes by A are visible to B
- A is ordered before B
- Program Order: Within a thread, statement 1 happens-before statement 2
- Monitor Lock: Unlock happens-before subsequent lock on same monitor
- volatile: Write to volatile happens-before read of that volatile
- Thread Start:
thread.start()happens-before any action in that thread - Thread Join: Actions in thread happen-before
thread.join()returns - Transitivity: If A → B and B → C, then A → C
// Thread 1
x = 42; // 1
volatile flag = true; // 2
// Thread 2
if (flag) { // 3
int y = x; // 4 - sees x = 42
}Happens-before chain: 1 → 2 → 3 → 4
Explains visibility bugs:
// Without volatile
boolean ready = false;
int value = 0;
// Thread 1
value = 42;
ready = true; // may be reordered before value = 42
// Thread 2
if (ready) {
System.out.println(value); // may print 0!
}| Aspect | sleep() | wait() |
|---|---|---|
| Class | Thread | Object |
| Lock | Doesn't release | Releases lock |
| Wake | After time | notify()/notifyAll() |
| Use | Pause execution | Condition waiting |
- Prevents CPU caching of variable
- Flush writes to main memory immediately
- Read from main memory (not cache)
- Memory barrier prevents reordering
- Make it immutable
- Use synchronized methods/blocks
- Use concurrent collections
- Use atomic variables
- Use thread confinement (ThreadLocal)
notify()wakes random thread (may wake wrong one)notifyAll()wakes all (they compete for lock)- Safer: avoids missed signals
Java 8+:
- CAS for most operations
- Synchronized for conflicts
- Tree bins for hash collisions (> 8 elements)
- No segment locking
- CPU-bound: cores + 1
- I/O-bound: cores × (1 + wait/compute)
- Mixed: Benchmark and tune
Multiple threads access shared data concurrently, and outcome depends on timing/interleaving of operations.
// Race condition
if (count == 0) {
count++; // another thread may increment here
}Thread unable to gain regular access to shared resources (lower priority, unfair scheduling).
Threads actively respond to each other but make no progress (both keep yielding to the other).
- Method: Locks entire object (
this) - Block: Can lock specific object, finer granularity
- "Thread safety is about correctness, not performance"
- "Concurrency bugs are non-deterministic and hard to reproduce"
- "Synchronization is about memory visibility, not just mutual exclusion"
- "Prefer immutability > thread confinement > synchronization"
- "Always use thread pools in production, never create threads manually"
- "Deadlock requires all four Coffman conditions simultaneously"
- "volatile ensures visibility, AtomicInteger ensures atomicity"
- "wait() must be in a while loop, not if, due to spurious wakeups"
- More threads than CPU cores without I/O
- Excessive locking (worse than single-threaded)
- Simple logic where overhead > benefit
- I/O-bound misuse (use async I/O instead)
- Debugging nightmare for marginal gains
| Concept | Key Takeaway |
|---|---|
| Thread | Lightweight, shares memory |
| synchronized | Simple mutual exclusion |
| ReentrantLock | Advanced features (try-lock, fairness) |
| volatile | Visibility, not atomicity |
| Atomic | Lock-free atomicity |
| wait/notify | Inter-thread communication |
| Deadlock | Lock ordering prevents it |
| Thread Pool | Production-ready concurrency |
| Semaphore | Resource pool control |
| Monitor | Mutex + condition variables |
| Happens-Before | Memory visibility guarantee |
This guide covers the essential multithreading concepts for interviews and real-world development. Practice implementing these patterns and understand the tradeoffs between different approaches.