☕
Core Java Roadmap Phase 11
0 / 40 (0%)
☰ Syllabus Index Next: P12 →

Java Multithreading, Concurrency & Thread Synchronization

📂 Repository Module: Module 1_ Problem Solving & Java programming \ 01_MultiThreading_Demos
📖 Textbook Alignment: Volume I — Chapter 12: Concurrency (Threads, Locks, Conditions, and Thread Pools)
Phase 11

Java Multithreading & Concurrency Master Class

📂 Module 1_ Problem Solving & Java programming \\ 01_MultiThreading_Demos
📖 Core Java Textbook Alignment: Volume I — Chapter 12: Concurrency (Threads, Thread Pools, and Synchronizers)

11.1 What is Multithreading? Process vs Thread Architecture

📖 Cay S. Horstmann • Core Java Volume I — Chapter 12, Section 12.1 & 12.3 ("Threads & Synchronization")
“When multiple threads read and write the same mutable object without coordination, race conditions occur. In Java, every object has an intrinsic lock (monitor). Using synchronized blocks ensures mutual exclusion and enforces memory visibility across CPU cores.”
  • Lock Axiomwait() and notifyAll() MUST be called inside a synchronized block holding the object monitor.
⚡ Pro-Tip: Deadlock Prevention via Global Lock Ordering
Always acquire multiple locks in the identical global sequence across all threads to make deadlocks mathematically impossible!
💡 Real-World Analogy: E-Commerce Order Processing
Imagine an e-commerce customer places an order on Amazon:
• Task A: Send an order confirmation email.
• Task B: Update inventory stock in the database.
• Task C: Charge the customer's credit card.
• Task D: Generate an invoice PDF.
If done on a single thread sequentially, the user would stare at a spinning wheel for 15 seconds! Multithreading allows these independent tasks to execute concurrently.
Dimension Operating System Process Java Thread
Definition An independently executing program (e.g. your JVM instance, Chrome browser). An independent execution path within a process.
Address Space Separate isolated memory space. One process cannot access another's memory directly. All threads within the same JVM process share the same Heap memory.
Creation Cost Heavyweight; requires OS page table and security context setup. Much lighter than processes, though OS threads still consume ~1MB stack.
Communication Requires Inter-Process Communication (IPC, sockets, pipes). Fast; communicates directly through shared Java objects on the Heap.

11.2 Creating Threads: Extending Thread vs Implementing Runnable

Why should we almost always use Runnable instead of extending Thread?
Runnable is an interface representing a task to execute, while Thread represents the worker worker mechanism.

Approach A: Extending Thread Approach B: Implementing Runnable
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running");
    }
}
// Usage:
MyThread t = new MyThread();
t.start();
class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Running");
    }
}
// Usage:
Thread t = new Thread(new MyTask());
t.start();
📖 4 Reasons Why Senior Developers Prefer Runnable:
1. Java Single Inheritance Rule: If you do class Employee extends Thread, your Employee class can NEVER inherit from Person or any other class! Implementing Runnable preserves your ability to extend another domain class.
2. Separation of Concerns: Runnable describes WHAT to do (the task). Thread describes HOW to run it (the worker). Don't couple your business logic to a thread lifecycle.
3. Task Reusability: The same Runnable instance can be submitted to 10 different threads simultaneously or run on multiple worker threads.
4. Thread Pool Integration: In production Spring Boot backends, you NEVER create raw new Thread() objects. You submit tasks to an ExecutorService thread pool. ExecutorService accepts Runnable and Callable, not custom Thread subclasses!
❓ Crucial Interview Question: What happens if you call t.run() instead of t.start()? ▼
Answer:
• t.start() asks the JVM and OS to spawn a brand new operating system thread. That new thread allocates its own call stack and executes the run() method asynchronously in the background.
• t.run() is just a regular, ordinary method call! It executes the code directly on the CURRENT thread (like the main thread) synchronously, blocking the caller. No new thread is spawned!
• Trap: Calling start() more than once on the same Thread instance throws IllegalThreadStateException.

11.3 The 6 Official Thread Lifecycle States (Thread.State)

🖊 Architectural State Diagram: Java Thread Lifecycle States (Thread.State) Mermaid Diagram
graph TD NEW["1. NEW
(Thread created; start() not yet called)"] RUNNABLE["2. RUNNABLE
(Executing in JVM / Ready in OS scheduler queue)"] BLOCKED["3. BLOCKED
(Waiting to acquire synchronized monitor lock)"] WAITING["4. WAITING
(Waiting indefinitely: wait(), join(), park())"] TIMED_WAITING["5. TIMED_WAITING
(Waiting with timeout: sleep(ms), wait(timeout))"] TERMINATED["6. TERMINATED
(run() completed or unhandled exception died)"] NEW -->|thread.start()| RUNNABLE RUNNABLE -->|Contended synchronized block| BLOCKED BLOCKED -->|Lock acquired| RUNNABLE RUNNABLE -->|wait() / join()| WAITING WAITING -->|notify() / notifyAll()| RUNNABLE RUNNABLE -->|Thread.sleep(ms)| TIMED_WAITING TIMED_WAITING -->|Time elapsed| RUNNABLE RUNNABLE -->|Completes| TERMINATED

8.4 The Danger: Race Conditions, synchronized, volatile & AtomicInteger

💡 Real-World Analogy: Why "count++" is Not Thread-Safe
Imagine two roommates updating a shared grocery list on the fridge that says Eggs = 10.
Roommate A looks at the paper (Reads 10), decides to add 1 (Calculates 11).
Before Roommate A writes it down, Roommate B looks at the paper (also reads 10!) and decides to add 1 (Calculates 11).
Roommate A writes 11. Roommate B writes 11.
Two eggs were added, but the final count is 11, not 12!
In Java bytecode, count++ is NOT one operation. It is 3 operations: (1) Read, (2) Increment, (3) Write. When two threads interleave, updates get lost!
Approach Code Sample Atomicity? Visibility? When to Use in Production
Synchronized synchronized void add() { count++; } Yes Yes Protecting multiple related variables or complex invariants.
AtomicInteger count.incrementAndGet(); Yes Yes High-throughput standalone counters without lock overhead (uses CPU CAS).
volatile volatile boolean running = true; NO (only 1-op reads/writes) Yes Status flag indicators (e.g. cancellation token). NOT for counters!
📂 Module 1.../01_MultiThreading_Demos/SavingAccount.java — Preventing Race Conditions
package com.accenture.ltt.multithreading;

public class SavingAccount {
    private double balance;

    public SavingAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // Synchronized method: Acquires the intrinsic lock of 'this' instance
    public synchronized void withdraw(double amount) {
        if (balance >= amount) {
            System.out.println(Thread.currentThread().getName() + " withdrawing: $" + amount);
            balance -= amount;
        } else {
            System.out.println("Insufficient balance for " + Thread.currentThread().getName());
        }
    }

    public synchronized double getBalance() {
        return balance;
    }
}
👁 Synchronized Methods & The Intrinsic Object Monitor Lock
  • Intrinsic Lock (Monitor) Every object on the Java Heap has an internal Monitor Lock. When a thread enters public synchronized void withdraw(...), it acquires this lock for this account instance.
  • Mutual Exclusion (Mutex) If Thread B attempts to call withdraw() while Thread A is inside the method, Thread B is immediately placed into the BLOCKED state. It sleeps until Thread A finishes and releases the lock.
  • Memory Visibility (Happens-Before) Exiting a synchronized block establishes a Happens-Before relationship. Any memory writes (like balance -= amount) are immediately flushed out of CPU L1/L2 caches back into main RAM, ensuring Thread B sees the fresh balance!

8.5 ExecutorService, Thread Pools & Java 21 Virtual Threads

Feature Platform Threads (Traditional) Virtual Threads (Java 21+)
OS Mapping 1:1 mapping with heavy OS kernel thread. M:N mapping. Millions of virtual threads scheduled by JVM onto few carrier threads.
Memory Footprint ~1MB stack per thread allocated in OS memory. ~1KB initially, grows dynamically on Java heap!
Max Threads A few thousand before OS crashes with OutOfMemoryError. Millions of concurrent threads easily on standard servers!
Best Used For CPU-heavy mathematical computations. I/O-heavy Spring Boot microservices waiting on DB queries and HTTP calls.
📂 VirtualThreadsDemo.java — Java 21 Virtual Thread Per Task
import java.util.concurrent.Executors;

public class VirtualThreadsDemo {
    public static void main(String[] args) {
        // Java 21: Auto-managed virtual thread executor
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 1; i <= 10_000; i++) {
                final int taskId = i;
                executor.submit(() -> {
                    // Simulates blocking I/O: Virtual thread unmounts from carrier thread during sleep!
                    Thread.sleep(1000);
                    System.out.println("Virtual task " + taskId + " completed on: " + Thread.currentThread());
                    return taskId;
                });
            }
        } // Auto-waits for all 10,000 tasks upon try-with-resources exit
    }
}
👁 Java 21 Project Loom Virtual Threads Under the Hood
  • Carrier Threads & ForkJoinPool Virtual threads do NOT map 1:1 to OS threads. The JVM manages an underlying ForkJoinPool of few OS "carrier" threads (equal to CPU core count).
  • Unmounting on Blocking I/O Line 11: Thread.sleep(1000) or any blocking socket/database call causes the virtual thread to unmount from the carrier thread! The carrier thread is instantly freed to execute other tasks.
  • Heap Stacks Virtual thread call frames live on the Java Heap, starting at only ~1 KB instead of a fixed 1 MB OS stack. This allows 1,000,000 concurrent threads to run on a standard 4 GB RAM server without crashing!
📂 StreamPipelineCompleteDemo.java — Filter, Map, FlatMap & GroupingBy
import java.util.*;
import java.util.stream.Collectors;

public class StreamPipelineCompleteDemo {
    record Employee(String name, String dept, double salary, List<String> skills) {}

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Alice", "IT", 85000, List.of("Java", "Docker")),
            new Employee("Bob", "HR", 62000, List.of("Recruiting")),
            new Employee("Charlie", "IT", 92000, List.of("Spring", "Kubernetes"))
        );

        // 1. Filter, Map & Collect: IT high-earners
        List<String> itStars = employees.stream()
            .filter(e -> e.dept().equals("IT") && e.salary() > 80000)
            .map(Employee::name)
            .collect(Collectors.toList());
        System.out.println("IT Stars: " + itStars);

        // 2. FlatMap: Flatten multiple lists of skills into one unique stream
        Set<String> allSkills = employees.stream()
            .flatMap(e -> e.skills().stream())
            .collect(Collectors.toSet());
        System.out.println("Distinct Skills: " + allSkills);

        // 3. Collectors.groupingBy: Group employees by Department
        Map<String, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::dept));
        System.out.println("Grouped By Dept: " + byDept.keySet());
    }
}
👁 Streams Pipeline: Filter, Map, FlatMap & GroupingBy Breakdown
  • filter() & map() Lines 8-12: filter takes a Predicate and drops non-matching items. map takes a Function and transforms each item (e.g. Employee -> String). Both operations are lazy: nothing runs until collect() is called.
  • flatMap() (1-to-Many Flattening) Line 15: Each employee has a list of skills. Calling map(e -> e.skills()) would yield a nested Stream<List<String>>. flatMap opens each list and merges all individual skill strings into a single unified Stream<String>.
  • Collectors.groupingBy() Line 21: Equivalent to SQL GROUP BY dept. Partitions the stream elements into a Map<K, List<T>> keyed by the classifier function. High-value feature for reporting engines and dashboard aggregations.
📂 OptionalCompleteDemo.java — Eliminating NullPointerException
import java.util.Optional;

public class OptionalCompleteDemo {
    public static Optional<String> findUserEmail(String userId) {
        if ("user_101".equals(userId)) {
            return Optional.of("john@accenture.com");
        }
        return Optional.empty(); // Returns clean Optional, NEVER null!
    }

    public static void main(String[] args) {
        // 1. orElse: Safe default fallback value
        String email = findUserEmail("user_999").orElse("no-reply@accenture.com");
        System.out.println("Email with fallback: " + email);

        // 2. map & ifPresent: Fluent chain without if-checks
        findUserEmail("user_101")
            .map(String::toUpperCase)
            .ifPresent(e -> System.out.println("Uppercase Email: " + e));

        // 3. orElseThrow: Standard Spring Boot pattern for 404 Not Found
        try {
            findUserEmail("unknown").orElseThrow(() -> new RuntimeException("User Not Found"));
        } catch (Exception e) {
            System.err.println("Caught Expected Exception: " + e.getMessage());
        }
    }
}
👁 java.util.Optional Best Practices & Production Anti-Patterns
  • Primary Purpose Optional<T> was designed exclusively as a method return type to clearly signal that a result might be absent, forcing callers to handle missing data explicitly without guessing.
  • Anti-Pattern: get() without isPresent() Never call optional.get() blindly! If empty, it throws NoSuchElementException, trading one crash for another. Prefer .orElse(), .orElseGet(supplier), or .orElseThrow().
  • Anti-Pattern: Optional as Field or Parameter Never use Optional as a class field (it is not Serializable) or as method arguments (just pass null or overload the method instead).
📂 CompletableFutureAsyncDemo.java — Non-Blocking Asynchronous Pipelines
import java.util.concurrent.CompletableFuture;

public class CompletableFutureAsyncDemo {
    public static void main(String[] args) {
        // Async Pipeline: Fetch price, apply tax, and print without blocking main thread!
        CompletableFuture<Double> pipeline = CompletableFuture.supplyAsync(() -> {
            // Stage 1: Async REST / DB call
            System.out.println("Stage 1 fetching base price on: " + Thread.currentThread().getName());
            return 100.0;
        }).thenApply(basePrice -> {
            // Stage 2: Transform result (calculate 18% tax)
            System.out.println("Stage 2 applying tax on: " + Thread.currentThread().getName());
            return basePrice * 1.18;
        }).exceptionally(ex -> {
            // Error handler fallback
            System.err.println("Fallback error: " + ex.getMessage());
            return 0.0;
        });

        // join() blocks only at the end to retrieve computed result
        System.out.println("Final Total: $" + pipeline.join());
    }
}
👁 CompletableFuture Non-Blocking Asynchronous Stage Pipeline
  • supplyAsync() Runs the supplier asynchronously on the shared ForkJoinPool.commonPool() (or custom executor), returning a promise of future completion without freezing the main thread.
  • thenApply() (Callback Chaining) Attaches a transformation stage that executes automatically when the prior stage completes. You don't have to poll or write messy nested callbacks.
  • exceptionally() (Resilience) Catches exceptions thrown anywhere along the pipeline and produces a safe fallback value, ensuring your web service doesn't crash on downstream timeout.
  • join() vs get() join() retrieves the value like Future.get(), but throws unchecked CompletionException instead of checked ExecutionException, allowing clean integration with lambdas.
📂 SignalWaitNotifyDemo.java — wait(), notifyAll() & ReentrantLock
import java.util.concurrent.locks.ReentrantLock;

public class SignalWaitNotifyDemo {
    private boolean ready = false;
    private final ReentrantLock lock = new ReentrantLock();

    // Coordination with wait() / notifyAll(): MUST use while loop to guard against spurious wakeups!
    public synchronized void waitForSignal() throws InterruptedException {
        while (!ready) {
            wait(); // Releases monitor lock and suspends thread!
        }
        System.out.println(Thread.currentThread().getName() + " woke up: Signal received!");
    }

    public synchronized void sendSignal() {
        ready = true;
        notifyAll(); // Wakes up all waiting threads!
    }
}
👁 Thread Coordination (wait/notifyAll) & The Spurious Wakeup Rule
  • The Spurious Wakeup Law Line 7: while (!ready) { wait(); } — Always call wait() inside a while loop, NEVER an if statement! Operating system threads can wake up spontaneously without any call to notify() (spurious wakeup). The while loop re-checks the predicate upon waking.
  • Lock Release Calling wait() releases the synchronized monitor lock and places the calling thread into the WAITING pool.
  • notify() vs notifyAll() notify() wakes only 1 arbitrary thread from the waiting pool (can cause thread starvation or lost signals). notifyAll() wakes all waiting threads, allowing them to contend fairly. Senior developers almost universally prefer notifyAll().
📂 Module 1.../01_MultiThreading_Demos/.../SenderReceiverTest.java
package com.accenture.ltt.threading.sample;

// Shared Data Monitor between Sender (Producer) and Receiver (Consumer)
class Data {
    private String packet;
    // true = receiver waits; false = sender waits
    private boolean transfer = true;

    public synchronized void send(String packet) {
        while (!transfer) {
            try {
                wait(); // Sender waits until receiver has processed previous packet
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        transfer = false;
        this.packet = packet;
        notifyAll(); // Wakes up receiver
    }

    public synchronized String receive() {
        while (transfer) {
            try {
                wait(); // Receiver waits until sender supplies packet
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        transfer = true;
        notifyAll(); // Wakes up sender
        return packet;
    }
}

public class SenderReceiverTest {
    public static void main(String[] args) {
        Data sharedData = new Data();
        new Thread(() -> {
            String[] packets = { "Packet 1", "Packet 2", "End" };
            for (String p : packets) sharedData.send(p);
        }).start();

        new Thread(() -> {
            String p = sharedData.receive();
            while (!"End".equals(p)) {
                System.out.println("Received: " + p);
                p = sharedData.receive();
            }
        }).start();
    }
}
👁 Producer-Consumer wait() / notifyAll() State Synchronization
  • Shared Monitor Lock Both send() and receive() are synchronized on the same shared Data object instance, preventing concurrent read/write corruption.
  • Flag-Driven Wait Condition The boolean transfer toggles back and forth. Sender waits when !transfer is true; Receiver waits when transfer is true. notifyAll() signals the other waiting thread to proceed.
📂 Module 1.../01_MultiThreading_Demos/.../ThreadJoinDemo.java
package com.accenture.ltt.threading.sample;

public class ThreadJoinDemo extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("Worker thread computing step: " + i);
            try { Thread.sleep(500); } catch (InterruptedException e) {}
        }
        System.out.println("Worker thread finished!");
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadJoinDemo worker = new ThreadJoinDemo();
        worker.start();

        // join() forces the calling thread (main) to wait until 'worker' terminates!
        worker.join();

        System.out.println("Main thread resumes after worker completed!");
    }
}
👁 Thread.join() Lifecycle Synchronization Mechanics
  • join() Blocks the Caller Line 19: When the main thread calls worker.join(), main moves from RUNNABLE to the WAITING state.
  • Automatic Wakeup on Termination When worker completes its run() method and enters TERMINATED, the JVM automatically wakes the waiting main thread to resume execution.
📂 ExecutorServicePoolDemo.java — Production Thread Pool Reuse
import java.util.concurrent.*;

public class ExecutorServicePoolDemo {
    public static void main(String[] args) {
        // Reusable Fixed Pool of 3 threads processing 6 submitted tasks
        ExecutorService pool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 1; i <= 6; i++) {
                final int taskId = i;
                pool.submit(() -> {
                    System.out.println("Task " + taskId + " running on worker: " + Thread.currentThread().getName());
                });
            }
        } finally {
            pool.shutdown(); // Always shutdown pools to prevent thread leaks!
        }
    }
}
👁 Production Thread Pools & Lifecycle Management
  • Thread Reuse Creating a thread requires allocating 1MB of memory and making an OS kernel system call. Thread pools maintain a fixed pool of live worker threads that pick tasks from an internal BlockingQueue, eliminating thread creation latency.
  • Lifecycle (shutdown vs shutdownNow) shutdown() allows currently running and queued tasks to finish, but refuses new submissions. shutdownNow() attempts to stop actively executing tasks by sending Thread.interrupt(). Always call shutdown in a finally block to prevent JVM shutdown hangs.
  • ThreadPoolExecutor Configuration In production Spring Boot backends, avoid unbounded newCachedThreadPool() which can spawn 10,000 threads and crash your server. Configure explicit corePoolSize, maxPoolSize, and a bounded ArrayBlockingQueue with a RejectedExecutionHandler.