Phase 11
Java Multithreading & Concurrency Master Class
📂 Module 1_ Problem Solving & Java programming \\ 01_MultiThreading_Demos
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.
• 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 |
|---|---|
|
|
📖 4 Reasons Why Senior Developers Prefer Runnable:
1. Java Single Inheritance Rule: If you do
2. Separation of Concerns:
3. Task Reusability: The same
4. Thread Pool Integration: In production Spring Boot backends, you NEVER create raw
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:
•
•
• Trap: Calling
•
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
(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
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,
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 forthisaccount 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
ForkJoinPoolof 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:
filtertakes aPredicateand drops non-matching items.maptakes aFunctionand transforms each item (e.g.Employee -> String). Both operations are lazy: nothing runs untilcollect()is called. -
flatMap() (1-to-Many Flattening)
Line 15: Each employee has a list of skills. Calling
map(e -> e.skills())would yield a nestedStream<List<String>>.flatMapopens each list and merges all individual skill strings into a single unifiedStream<String>. -
Collectors.groupingBy()
Line 21: Equivalent to SQL
GROUP BY dept. Partitions the stream elements into aMap<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 throwsNoSuchElementException, trading one crash for another. Prefer.orElse(),.orElseGet(supplier), or.orElseThrow(). -
Anti-Pattern: Optional as Field or Parameter
Never use
Optionalas a class field (it is notSerializable) or as method arguments (just passnullor 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 likeFuture.get(), but throws uncheckedCompletionExceptioninstead of checkedExecutionException, 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 tonotify()(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 prefernotifyAll().
📂 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()andreceive()are synchronized on the same sharedDataobject instance, preventing concurrent read/write corruption. -
Flag-Driven Wait Condition
The boolean
transfertoggles back and forth. Sender waits when!transferis true; Receiver waits whentransferis 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
mainthread callsworker.join(),mainmoves from RUNNABLE to the WAITING state. -
Automatic Wakeup on Termination
When
workercompletes itsrun()method and enters TERMINATED, the JVM automatically wakes the waitingmainthread 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 sendingThread.interrupt(). Always call shutdown in afinallyblock 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 explicitcorePoolSize,maxPoolSize, and a boundedArrayBlockingQueuewith aRejectedExecutionHandler.