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.
thread.join(), sleep(), yield()¶
join() — calling thread waits until the target thread finishes. t.join() blocks the current thread until t completes. sleep(ms) — pauses current thread for specified milliseconds, does NOT release locks. yield() — hints scheduler to let other threads run (rarely used, not guaranteed). interrupt() — sets interrupt flag; sleeping/waiting threads throw InterruptedException.
Deep Dive: join() Example & Comparison
Thread t1 = new Thread(() -> {
System.out.println("T1 working...");
try { Thread.sleep(2000); } catch (InterruptedException e) { }
System.out.println("T1 done");
});
Thread t2 = new Thread(() -> {
try {
t1.join(); // T2 waits for T1 to finish
} catch (InterruptedException e) { }
System.out.println("T2 starts after T1");
});
t1.start();
t2.start();
// Output: T1 working... → T1 done → T2 starts after T1
// join with timeout — wait at most 3 seconds
t1.join(3000);
| Method | Purpose | Releases Lock? | Thread State |
|---|---|---|---|
join() |
Wait for another thread to finish | N/A | WAITING |
join(ms) |
Wait with timeout | N/A | TIMED_WAITING |
sleep(ms) |
Pause current thread | ❌ No | TIMED_WAITING |
yield() |
Hint to scheduler | ❌ No | RUNNABLE |
wait() |
Wait for notify (with synchronized) |
✅ Yes | WAITING |
Common interview question — sleep() vs wait():
sleep()— pauses thread, does NOT release the lock, resumes after timeoutwait()— releases the lock, must be called insidesynchronized, resumes onnotify()
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?