Multithreading¶
Java supports multithreading via Thread, Runnable, ExecutorService, and CompletableFuture. Synchronization primitives: synchronized, ReentrantLock, volatile, Atomic classes. Key concepts: thread lifecycle, thread pools, wait/notify, CountDownLatch, CyclicBarrier. Prefer ExecutorService over raw threads.
Thread Creation¶
Three ways: 1) Extend Thread (not recommended — no composition), 2) Implement Runnable (preferred — separates task from thread), 3) ExecutorService (best practice — manages thread pools, lifecycle, task queuing). Never create raw threads in production — use thread pools.
Deep Dive: Creation Methods
// 1. Extend Thread
class MyThread extends Thread {
public void run() { System.out.println("Running"); }
}
new MyThread().start();
// 2. Implement Runnable (preferred — composition over inheritance)
Runnable task = () -> System.out.println("Running");
new Thread(task).start();
// 3. ExecutorService (best practice)
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task running"));
executor.shutdown();
Thread Lifecycle¶
Six states: NEW (created, not started), RUNNABLE (ready/running), BLOCKED (waiting for lock), WAITING (waiting indefinitely — wait(), join()), TIMED_WAITING (waiting with timeout — sleep(ms)), TERMINATED (completed). start() moves NEW → RUNNABLE; calling run() directly does NOT create a new thread.
Deep Dive: State Machine
Key difference: start() creates a new thread and calls run() on it. Calling run() directly executes in the current thread.
synchronized vs ReentrantLock¶
synchronized — built-in, auto-unlock, simpler. ReentrantLock — more flexible: supports fairness, tryLock() (non-blocking), lockInterruptibly(), and multiple Condition variables. Use synchronized by default; use ReentrantLock when you need advanced features.
Deep Dive: Comparison & Examples
// synchronized — method-level
public synchronized void increment() { count++; }
// synchronized — block-level
public void increment() {
synchronized (this) { count++; }
}
// ReentrantLock — more control
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try { count++; }
finally { lock.unlock(); } // Always unlock in finally
}
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Auto unlock | Yes | No (must use try-finally) |
| Fairness | No | Yes (optional) |
| Try lock | No | Yes (tryLock()) |
| Interruptible | No | Yes (lockInterruptibly()) |
| Condition variables | 1 (wait/notify) |
Multiple (Condition) |
volatile Keyword¶
volatile ensures visibility — writes by one thread are immediately visible to others (bypasses CPU cache). It also prevents instruction reordering around the variable. However, volatile does NOT make compound operations (like count++) atomic — for that, use Atomic classes.
Deep Dive: What volatile Does and Doesn't Do
private volatile boolean running = true;
// Thread 1
public void stop() {
running = false; // Immediately visible to Thread 2
}
// Thread 2
public void run() {
while (running) { /* work */ } // Always reads fresh value
}
NOT atomic:
Atomic Classes¶
Atomic classes (AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference) provide lock-free thread-safe operations using CAS (Compare-And-Swap). Faster than synchronized for simple operations. incrementAndGet(), compareAndSet(), and getAndUpdate() are all atomic.
Deep Dive: CAS and Examples
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // Atomically: return ++counter
counter.getAndIncrement(); // Atomically: return counter++
// Compare and set — update only if current value matches expected
boolean updated = counter.compareAndSet(5, 10);
// Sets to 10 only if current value is 5
CAS internally: Read current → compute new → if current == expected, write new; else retry. This is a single CPU instruction — no locking needed.
ExecutorService & Thread Pools¶
Thread pools reuse threads (avoiding creation overhead) and limit concurrency. Types: FixedThreadPool (N threads, tasks queue), CachedThreadPool (dynamic, creates as needed), SingleThreadExecutor (sequential), ScheduledThreadPool (delayed/periodic tasks). Always call shutdown() to terminate.
Deep Dive: Pool Types and Lifecycle
// Fixed — N threads, excess tasks queue
ExecutorService fixed = Executors.newFixedThreadPool(5);
// Cached — dynamic, reuses idle threads
ExecutorService cached = Executors.newCachedThreadPool();
// Single — sequential execution
ExecutorService single = Executors.newSingleThreadExecutor();
// Scheduled — delayed and periodic tasks
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
scheduled.schedule(task, 5, TimeUnit.SECONDS); // Run after 5s
scheduled.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS); // Every 1s
Lifecycle:
wait/notify vs Lock/Condition¶
wait()/notify() work with synchronized — one condition per lock. Lock/Condition is more flexible — multiple conditions per lock, interruptible wait, timed wait. Always use while loop (not if) around wait()/await() to guard against spurious wakeups.
Deep Dive: Comparison
// wait/notify (with synchronized)
synchronized (lock) {
while (!ready) { lock.wait(); } // Releases lock and waits
}
synchronized (lock) { ready = true; lock.notify(); }
// Lock/Condition (more flexible)
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try { while (!ready) { condition.await(); } }
finally { lock.unlock(); }
lock.lock();
try { ready = true; condition.signal(); }
finally { lock.unlock(); }
Why while, not if?
CountDownLatch vs CyclicBarrier¶
CountDownLatch — one thread waits for N events/tasks to complete. One-time use. CyclicBarrier — N threads wait for each other at a rendezvous point. Reusable. Use CountDownLatch for "wait for completion"; CyclicBarrier for "synchronize at phase boundaries".
Deep Dive: Examples
// CountDownLatch — main thread waits for 3 workers
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
executor.submit(() -> { /* work */ latch.countDown(); });
}
latch.await(); // Blocks until count reaches 0
// CyclicBarrier — 3 threads synchronize
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All reached barrier");
});
for (int i = 0; i < 3; i++) {
executor.submit(() -> { /* phase 1 */ barrier.await(); /* phase 2 */ });
}
| CountDownLatch | CyclicBarrier | |
|---|---|---|
| Purpose | Wait for N events | Synchronize N threads |
| Reusable | No | Yes |
| Callback | No | Yes (barrier action) |
CompletableFuture¶
CompletableFuture enables asynchronous computation with chaining — like Promises in JavaScript. Key methods: supplyAsync() (start async), thenApply() (transform), thenAccept() (consume), thenCombine() (combine two futures), exceptionally() (error handling). Much more powerful than Future (which only has blocking get()).
Deep Dive: Chaining and Combining
// Async computation
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
// Chain operations
future.thenApply(s -> s + " World")
.thenAccept(System.out::println); // "Hello World"
// Combine two futures
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> combined = f1.thenCombine(f2, Integer::sum); // 30
// Exception handling
CompletableFuture<String> safe = future
.exceptionally(ex -> "Default value");
// Blocking get (avoid in production)
String result = future.get(); // or get(timeout, unit)
Common Interview Questions¶
Common Interview Questions
- What is the difference between
ThreadandRunnable? - What is the difference between
start()andrun()? - Explain the thread lifecycle states.
- What is the difference between
synchronizedandReentrantLock? - What does the
volatilekeyword do? - Why use
AtomicIntegerinstead ofsynchronizedfor a counter? - What is a thread pool? Why use
ExecutorService? - Why must
wait()be called inside awhileloop? - What is the difference between
CountDownLatchandCyclicBarrier? - What is
CompletableFuture? How is it different fromFuture?