Skip to content

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
NEW → start() → RUNNABLE ⇄ RUNNING
                BLOCKED / WAITING / TIMED_WAITING
                TERMINATED

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:

private volatile int count = 0;
public void increment() {
    count++;  // NOT atomic! (read → add → write)
    // Use AtomicInteger instead
}


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:

executor.shutdown();     // No new tasks, finish current ones
executor.shutdownNow();  // Attempt to stop all immediately
executor.awaitTermination(1, TimeUnit.MINUTES);  // Block until done


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?

// ❌ Wrong — spurious wakeups can cause bugs
if (!ready) lock.wait();

// ✅ Correct — re-check condition after wakeup
while (!ready) lock.wait();


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 Thread and Runnable?
  • What is the difference between start() and run()?
  • Explain the thread lifecycle states.
  • What is the difference between synchronized and ReentrantLock?
  • What does the volatile keyword do?
  • Why use AtomicInteger instead of synchronized for a counter?
  • What is a thread pool? Why use ExecutorService?
  • Why must wait() be called inside a while loop?
  • What is the difference between CountDownLatch and CyclicBarrier?
  • What is CompletableFuture? How is it different from Future?