To understand multithreading, one must differentiate between an OS process and an in-process execution unit (thread):
| 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 |
Thread Class vs. Runnable Interfacejava.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.
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).
Thread.State
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!new Thread()
start() ──➔run() finishes ──➔wait() / join()
sleep(ms) / wait(ms)
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.
Every Java thread is assigned an integer priority determining its scheduling preference:
Thread.MIN_PRIORITY = 1Thread.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.
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.
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.
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++;
}
}
wait(), notify() & notifyAll()
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(); }
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!
A deadlock requires all four Coffman conditions to hold simultaneously. Breaking any one condition guarantees deadlock immunity:
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.
java.util.concurrent)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.
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.
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("==================================================");
}
}
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.
Runnable favored over extending Thread?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.
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.
wait(), notify(), and notifyAll() defined in java.lang.Object instead of Thread?wait) or signal threads waiting on that monitor (notify) must be accessible on every object instance.
wait() always be called within a while loop instead of an if block?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.
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.
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).