☕ Unit 11 • Core Java Fundamentals

Multithreading & Concurrency: Threads, Monitors & Executors

Mastering concurrent execution in Java: Process vs. Thread memory footprints, thread instantiation via Thread and Runnable, the 6 JVM thread lifecycle states, execution control methods (start, sleep, join, yield), thread priorities, race conditions and monitor synchronization, inter-thread signaling (wait/notify), Deadlock conditions and avoidance, and thread pool scaling with the modern Executor Framework.

Curriculum: Enterprise Java Software Engineering
Unit: 11 of 15
Prerequisite: Unit 06 (OOP), Unit 07 (Exception Handling)
Target: JDK 21 LTS
01

Process versus Thread: Architectural Foundations

⚡ Operating System Level vs. In-Process Concurrency

To understand multithreading, one must differentiate between an OS process and an in-process execution unit (thread):

  • Process (Heavyweight): A self-contained execution environment managed by the Operating System. Each process possesses its own private, isolated virtual address space, memory pages, file handles, and security context. Inter-Process Communication (IPC) is expensive, requiring OS pipes, shared memory segments, or network sockets.
  • Thread (Lightweight): A sequential path of execution within a process. All threads running within a single JVM process share common memory spaces: the Heap (all objects) and Metaspace (class definitions). However, each thread retains its own private Call Stack, Program Counter (PC) Register, and Local Variable Array.
Visual Architecture: JVM Memory Isolation Across Concurrent Threads
SHARED JVM PROCESS SPACE
• Heap Memory (All Objects, Static Variables)
• Metaspace (Class Bytecode, Constant Pool)
• Open File Descriptors & Sockets
▼ shared by all concurrent threads ▼
Thread 1 (Main)
Private Call Stack
Private PC Register
Local Frames
Thread 2 (Worker-A)
Private Call Stack
Private PC Register
Local Frames
Thread 3 (Worker-B)
Private Call Stack
Private PC Register
Local Frames
Attribute Operating System Process JVM Thread
Memory Boundary Strictly isolated private address space Shares Heap & Metaspace with sibling threads
Context Switch Cost Heavy (MMU page table swap, CPU cache flush) Lightweight (Registers, Stack Pointer swap)
Communication Expensive IPC (Sockets, Pipes, Shared Memory) Fast in-memory communication (Shared objects, queues)
Failure Blast Radius Process crash does not kill other processes Unhandled exception in thread can destabilize process
02

Creating Threads: Thread Class vs. Runnable Interface

1️⃣ Extending java.lang.Thread

Subclasses inherit the Thread class and override the public void run() method:

class Worker extends Thread {
    @Override
    public void run() {
        System.out.println("Worker executing in " + getName());
    }
}
// Invocation
Worker t = new Worker();
t.start();

Limitation: Java forbids multiple class inheritance. Extending Thread burns the single inheritance slot, preventing the class from subclassing any other domain entity.

2️⃣ Implementing java.lang.Runnable (Best Practice)

Separates the task unit from the execution engine (Composition over Inheritance):

class Task implements Runnable {
    @Override
    public void run() {
        System.out.println("Task running on " + Thread.currentThread().getName());
    }
}
// Or Modern Java Lambda syntax:
Thread t = new Thread(() -> System.out.println("Lambda worker"));
t.start();

Advantages: Adheres to Single Responsibility Principle, leaves inheritance open, and enables seamless integration with thread pools (ExecutorService).

03

Thread Life Cycle: The 6 States of Thread.State

🔄 The JVM Thread State Machine

A thread in Java resides in exactly one of the six states enumerated by java.lang.Thread.State at any point during its lifetime:

  • NEW: The Thread instance has been instantiated (new Thread()), but start() has not yet been invoked. No OS native thread is allocated.
  • RUNNABLE: The thread is actively executing bytecode or waiting for CPU time-slice allocation from the OS scheduler. (Combines OS "Ready" and "Running").
  • BLOCKED: The thread is paused waiting to acquire an intrinsic monitor lock to enter or re-enter a synchronized block/method.
  • WAITING: The thread is waiting indefinitely for another thread to perform a specific action (triggered by Object.wait(), Thread.join(), or LockSupport.park()).
  • TIMED_WAITING: The thread is sleeping or waiting up to a designated timeout duration (triggered by Thread.sleep(ms), Object.wait(ms), or Thread.join(ms)).
  • TERMINATED: The thread has completed execution either by returning from its run() method or by throwing an uncaught exception. Cannot be restarted!
Visual Architecture: Java Thread Lifecycle State Transitions
NEW
new Thread()
── start() ──➔
RUNNABLE
Ready / In Execution
── run() finishes ──➔
TERMINATED
Dead / Finished
▲ Suspensions & Resume Cycles ▼
BLOCKED
Waiting for Monitor Lock
WAITING
wait() / join()
TIMED_WAITING
sleep(ms) / wait(ms)
04

Core Thread Control Methods & Thread Priorities

🛠️ Critical Thread API Methods

The Thread class provides primary control hooks over thread execution:

  • start() vs. run(): Calling start() commands the JVM to spawn a new native OS thread and register it with the OS scheduler, which subsequently triggers run() asynchronously. Calling run() directly does not create a new thread—it executes synchronously inside the caller's call stack!
  • sleep(long millis): Static method that pauses the currently executing thread for a specified duration. The thread enters TIMED_WAITING. Crucially, sleep() does NOT release any acquired monitor locks! Throws InterruptedException.
  • join() / join(long millis): Suspends the calling thread until the target thread on which join() was called finishes execution. Enables deterministic synchronization of dependent tasks.
  • yield(): A static hint to the OS thread scheduler that the current thread is willing to surrender its current CPU time slice to other threads of equal priority. The scheduler is free to ignore this hint.
🔢 Thread Priorities (1 to 10)

Every Java thread is assigned an integer priority determining its scheduling preference:

  • Thread.MIN_PRIORITY = 1
  • Thread.NORM_PRIORITY = 5 (Default assigned to new threads)
  • Thread.MAX_PRIORITY = 10

Configured via thread.setPriority(int level). Caution: Thread priority behavior is platform-dependent and mapped to host OS scheduling mechanisms. Never write production business logic that depends on priority levels for correctness.

05

Thread Synchronization & Monitor Locks

🔒 Race Conditions & Critical Sections

When multiple threads concurrently read and write a shared mutable variable without coordination, a race condition occurs. For example, count++ is not atomic; it consists of three distinct bytecode instructions: (1) read value into register, (2) increment value, and (3) write value back to heap. An intervening thread context switch between steps 1 and 3 produces corrupted data.

Synchronization enforces Mutual Exclusion (Mutex): only one thread can execute inside a protected critical section at any given moment.

1. Synchronized Methods

Locks the entire method body using the instance's intrinsic monitor (this):

public synchronized void deposit(double amount) {
    this.balance += amount;
}

For static synchronized methods, the lock is acquired on the Class object (e.g., Account.class), blocking all instances.

2. Synchronized Blocks (Granular)

Locks only the exact lines of code that access shared state, specifying an explicit lock object:

private final Object lock = new Object();

public void process() {
    // Non-critical operations (concurrent)
    synchronized (lock) {
        // Critical section (strictly serialized)
        counter++;
    }
}
06

Inter-Thread Communication: wait(), notify() & notifyAll()

🔔 The Object Monitor Signaling Protocol

Threads communicate state changes through methods defined on java.lang.Object (not Thread). Why? Because locks and wait sets belong to the memory object whose monitor is being held!

  • wait(): Causes the calling thread to release the monitor lock immediately and enter the object's Wait Set (state WAITING). It sleeps until another thread signals it.
  • notify(): Wakes up a single arbitrarily selected thread from the object's wait set, moving it to the BLOCKED state to contend for lock re-acquisition.
  • notifyAll(): Wakes up all threads waiting in the wait set. Preferred over notify() to prevent accidental signal loss in multi-consumer setups.

Mandatory Rule: wait(), notify(), and notifyAll() must be called inside a synchronized context (method or block). Invoking them without holding the monitor throws IllegalMonitorStateException.

The While-Loop Guard: Always invoke wait() inside a while loop, never an if statement, to defend against spurious wakeups:
while (!conditionMet) { wait(); }

07

Deadlock Fundamentals & Prevention Strategies

⚠️ What is a Deadlock? The Deadly Embrace

A Deadlock describes a situation where two or more threads are permanently blocked, each holding a lock that the other thread requires to proceed.

Example Scenario:
• Thread-1 locks Resource A and attempts to acquire Resource B.
• Concurrently, Thread-2 locks Resource B and attempts to acquire Resource A.
• Neither thread can proceed, and neither will release its lock → Permanent deadlock freeze!

Visual Architecture: Circular Lock Dependency Causing Deadlock
Thread-1
Holds Lock-A
Needs Lock-B
── Requests (Blocked) ──➔
Lock-B (Account 2)
Acquired by Thread-2
Lock-A (Account 1)
Acquired by Thread-1
◀── Requests (Blocked) ──
Thread-2
Holds Lock-B
Needs Lock-A
🛡️ The 4 Coffman Conditions & How to Prevent Deadlocks

A deadlock requires all four Coffman conditions to hold simultaneously. Breaking any one condition guarantees deadlock immunity:

  1. Mutual Exclusion: At least one resource must be held in a non-shareable mode.
  2. Hold and Wait: A thread holds a resource while waiting to acquire another.
  3. No Preemption: Resources cannot be forcibly revoked from a thread; they must be released voluntarily.
  4. Circular Wait: A closed chain of threads exists, where each thread holds a lock needed by the next.

Primary Prevention Rule (Lock Ordering): Always acquire multiple locks in a strict, global lexicographical or numerical order (e.g., sort accounts by ID before acquiring locks). This eliminates circular wait.

08

Introduction to the Executor Framework (java.util.concurrent)

🏭 Why Manual new Thread() Spawning Fails in Production

Spawning an unconstrained number of raw threads via new Thread().start() in server applications leads to disastrous consequences: each thread consumes roughly 1MB of stack memory and taxes the OS kernel with context-switching overhead. A sudden traffic spike can cause OutOfMemoryError: unable to create new native thread.

Introduced in Java 5 by Doug Lea, the Executor Framework abstracts thread lifecycle and management through reusable Thread Pools:

  • Executor: Simple functional interface defining void execute(Runnable command).
  • ExecutorService: Sub-interface providing lifecycle controls (submit, invokeAll, shutdown, awaitTermination).
  • Executors: Factory utility class providing pre-configured pool architectures:
    • Executors.newFixedThreadPool(int n): Reuses a fixed number of threads operating off a shared unbounded queue.
    • Executors.newCachedThreadPool(): Creates new threads as needed, reusing idle threads (idle threads terminate after 60s).
    • Executors.newSingleThreadExecutor(): Guarantees sequential execution of submitted tasks on a single dedicated worker thread.
09

Unit 11 Practical Capstone: Concurrent Buffer & Worker Pool

🚀 Complete Producer-Consumer & Executor Pipeline

This complete, verified program demonstrates:
1. A thread-safe bounded circular queue coordinating producers and consumers via wait() and notifyAll().
2. An ExecutorService pool executing asynchronous ingest and worker tasks with graceful shutdown.

ConcurrentPipelineSimulator.java — Complete Capstone Implementation
import java.util.*;
import java.util.concurrent.*;

// 1. Thread-Safe Bounded Message Queue (wait / notifyAll Protocol)
class BoundedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public BoundedBuffer(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void produce(T item) throws InterruptedException {
        while (queue.size() == capacity) {
            // Buffer full: Producer releases monitor and waits
            wait();
        }
        queue.add(item);
        System.out.printf("  [PRODUCER %s] Produced: %s (Queue size: %d)%n",
                Thread.currentThread().getName(), item, queue.size());
        notifyAll(); // Wake up waiting consumers
    }

    public synchronized T consume() throws InterruptedException {
        while (queue.isEmpty()) {
            // Buffer empty: Consumer releases monitor and waits
            wait();
        }
        T item = queue.poll();
        System.out.printf("  [CONSUMER %s] Consumed: %s (Remaining: %d)%n",
                Thread.currentThread().getName(), item, queue.size());
        notifyAll(); // Wake up waiting producers
        return item;
    }
}

// 2. Execution Controller
public class ConcurrentPipelineSimulator {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("==================================================");
        System.out.println("   DEVASCENT MULTITHREADING & EXECUTOR ENGINE     ");
        System.out.println("==================================================");

        BoundedBuffer<String> buffer = new BoundedBuffer<>(2); // Max 2 items in flight

        // Initialize Fixed Thread Pool with 4 worker threads
        ExecutorService executor = Executors.newFixedThreadPool(4);

        // Task: Producer
        Runnable producerTask = () -> {
            String[] payloads = {"PAYLOAD-101", "PAYLOAD-102", "PAYLOAD-103", "PAYLOAD-104"};
            for (String p : payloads) {
                try {
                    buffer.produce(p);
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        };

        // Task: Consumer
        Runnable consumerTask = () -> {
            for (int i = 0; i < 2; i++) {
                try {
                    buffer.consume();
                    Thread.sleep(120);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        };

        // Dispatch to Thread Pool
        System.out.println("--- Submitting Producer and Consumer Tasks ---");
        executor.submit(producerTask);
        executor.submit(consumerTask);
        executor.submit(consumerTask);

        // Graceful Shutdown
        executor.shutdown();
        boolean finished = executor.awaitTermination(3, TimeUnit.SECONDS);
        System.out.println("\nAll Pipeline Tasks Completed: " + finished);
        System.out.println("==================================================");
    }
}
Execution Result (javac & java)
$ javac ConcurrentPipelineSimulator.java $ java ConcurrentPipelineSimulator ================================================== DEVASCENT MULTITHREADING & EXECUTOR ENGINE ================================================== --- Submitting Producer and Consumer Tasks --- [PRODUCER pool-1-thread-1] Produced: PAYLOAD-101 (Queue size: 1) [CONSUMER pool-1-thread-2] Consumed: PAYLOAD-101 (Remaining: 0) [PRODUCER pool-1-thread-1] Produced: PAYLOAD-102 (Queue size: 1) [CONSUMER pool-1-thread-3] Consumed: PAYLOAD-102 (Remaining: 0) [PRODUCER pool-1-thread-1] Produced: PAYLOAD-103 (Queue size: 1) [PRODUCER pool-1-thread-1] Produced: PAYLOAD-104 (Queue size: 2) [CONSUMER pool-1-thread-2] Consumed: PAYLOAD-103 (Remaining: 1) [CONSUMER pool-1-thread-3] Consumed: PAYLOAD-104 (Remaining: 0) All Pipeline Tasks Completed: true ==================================================
10

University Exam & Technical Interview Viva Preparation Guide

Q1. What is the difference between Thread.start() and Thread.run()?
start() initiates an asynchronous OS system call that allocates an independent call stack, registers the thread with the operating system scheduler, and triggers the run() method on the newly created thread. If you call run() directly, no new thread is spawned; the method behaves like any ordinary Java method invocation, executing synchronously inside the current caller thread's stack frame.
Q2. Why is implementing Runnable favored over extending Thread?
Java enforces single inheritance: extending Thread permanently prevents the class from inheriting from any other superclass. Furthermore, implementing Runnable adheres to the Single Responsibility Principle by decoupling the actual computational task (the "what") from the physical execution thread worker (the "how"). It also facilitates lambda syntax and task reuse within thread pools like ExecutorService.
Q3. Does Thread.sleep() release monitor locks? What about Object.wait()?
Thread.sleep() does NOT release locks; the thread holds onto all acquired monitors and keeps other contending threads blocked while it sleeps. Conversely, Object.wait() immediately releases the monitor lock of the target object, enabling other threads to enter synchronized blocks on that object.
Q4. Why are wait(), notify(), and notifyAll() defined in java.lang.Object instead of Thread?
In Java, intrinsic monitor locks belong to objects on the heap, not to threads. Since any Java object can act as a monitor lock with its own associated Wait Set, the methods to release that monitor (wait) or signal threads waiting on that monitor (notify) must be accessible on every object instance.
Q5. Why must wait() always be called within a while loop instead of an if block?
Due to spurious wakeups, a thread can wake up from wait() without receiving an explicit notify() or notifyAll() signal. Additionally, in multi-consumer architectures, another thread may acquire the lock and invalidate the business condition before the awakened thread gets scheduled. Testing the condition inside a while loop ensures the thread re-verifies the invariant before proceeding.
Q6. What are the four Coffman conditions required for a Deadlock?
(1) Mutual Exclusion: Non-shareable resource locks.
(2) Hold and Wait: Holding one lock while waiting for another.
(3) No Preemption: Locks cannot be forcibly taken away.
(4) Circular Wait: A closed loop of threads where each holds a lock requested by the next.
Breaking any single condition (most commonly avoiding Circular Wait via strict lock ordering) guarantees deadlock freedom.
Q7. What is the difference between notify() and notifyAll()? When should each be used?
notify() wakes up an arbitrary, nondeterministic single thread from the wait set, which can lead to missed signals or "signal hijacking" if the chosen thread cannot proceed with current state. notifyAll() awakens all waiting threads, allowing them all to compete for the monitor and re-evaluate their predicates. notifyAll() is the recommended default in enterprise production code.
Q8. What are the advantages of using the Executor Framework over creating raw threads?
Creating raw threads incurs heavy CPU and memory allocation costs (~1MB stack per thread) and can trigger OutOfMemoryError under high load. The ExecutorFramework decouples task submission from execution, maintains a controlled, bounded pool of reusable threads, mitigates context-switching overhead, and provides standardized lifecycle management (task queues, timeouts, futures, and graceful shutdowns).