Loading content...
Every thread has a story—a journey from non-existence, through creation, execution, waiting, and ultimately termination. Understanding this lifecycle isn't just academic knowledge; it's operational necessity.
Why Thread Lifecycle Matters:
By the end of this page, you will understand the complete lifecycle of a thread—every state it can occupy, every transition it can make, and the triggers that cause each transition. You'll be able to reason about thread behavior in complex concurrent systems and diagnose lifecycle-related issues.
A thread moves through a finite set of states during its lifetime. While different operating systems and languages may name these states differently, the underlying model is consistent across platforms.
| State | Description | CPU Usage | Typical Duration |
|---|---|---|---|
| NEW | Thread object exists but start() not called. No OS thread allocated yet. | None | Application dependent |
| RUNNABLE | Thread is eligible to run. May be running or waiting for CPU in ready queue. | Ready for CPU | Depends on scheduling |
| RUNNING | Thread is actively executing instructions on a CPU core. | Active | Time slice (~1-10ms) |
| BLOCKED | Thread is waiting to acquire a monitor lock held by another thread. | None | Until lock released |
| WAITING | Thread waits indefinitely for another thread to perform an action. | None | Indefinite |
| TIMED_WAITING | Thread waits for a specified time period or until notified. | None | Until timeout/notify |
| TERMINATED | Thread has completed execution. Cannot be restarted. | None | N/A |
Many thread APIs (like Java's Thread.State) combine RUNNABLE and RUNNING into a single RUNNABLE state because the distinction is managed by the OS scheduler, not the language runtime. A RUNNABLE thread is either currently running or ready to run—the application cannot directly control which.
A thread begins its lifecycle in the NEW state. At this point, the thread object exists in your program's memory, but the operating system has not yet allocated the resources needed for execution.
What Happens During Thread Creation:
At this point, no OS-level thread exists. The thread cannot run. The NEW state is a preparation phase.
12345678910111213141516171819202122232425262728293031323334
import java.lang.Thread.State; public class ThreadCreationDemo { public static void main(String[] args) { // Method 1: Extend Thread class Thread thread1 = new Thread() { @Override public void run() { System.out.println("Thread 1 running"); } }; // Method 2: Implement Runnable interface (preferred) Runnable task = () -> { System.out.println("Thread 2 running"); }; Thread thread2 = new Thread(task, "WorkerThread"); // Thread is in NEW state - OS thread not created yet System.out.println("Thread 1 state: " + thread1.getState()); // NEW System.out.println("Thread 2 state: " + thread2.getState()); // NEW // Configure before starting thread2.setPriority(Thread.MAX_PRIORITY); thread2.setDaemon(false); // Transition to RUNNABLE - this creates OS thread thread1.start(); thread2.start(); System.out.println("Thread 1 state: " + thread1.getState()); // RUNNABLE System.out.println("Thread 2 state: " + thread2.getState()); // RUNNABLE }}A frequent beginner mistake is calling thread.run() instead of thread.start(). The run() method executes the thread's code in the CURRENT thread—no new thread is created. Only start() triggers the NEW → RUNNABLE transition and creates an actual OS thread.
When start() is called, the thread transitions from NEW to RUNNABLE. The operating system allocates the actual thread resources—stack memory, kernel data structures, thread ID—and places the thread in the scheduler's ready queue.
The RUNNABLE → RUNNING → RUNNABLE Cycle:
A RUNNABLE thread is not necessarily running at any given moment. The OS scheduler manages which threads actually execute. The cycle works as follows:
┌─────────────────────────────────────────────────────────────────────┐│ SCHEDULER OVERVIEW │└─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────┐ │ READY QUEUE │ │ ┌─────┬─────┬─────┬─────┐ │ │ │ T1 │ T3 │ T5 │ T8 │ │ RUNNABLE threads │ │ │ │ │ │ │ waiting for CPU │ └─────┴─────┴─────┴─────┘ │ └──────────────┬──────────────┘ │ ▼ Scheduler dispatch ┌─────────────────────────────┐ │ CPU CORE │ │ │ │ ┌─────────────────────┐ │ │ │ Thread T3 │ │ RUNNING │ │ (executing code) │ │ (active execution) │ └─────────────────────┘ │ │ │ └──────────────┬──────────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ RUNNABLE │ │ BLOCKED/ │ │ TERMINATED │ │ (back to │ │ WAITING │ │ (exit) │ │ ready queue)│ │ (removed from│ │ │ │ │ │ scheduling) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ Time slice I/O, lock, wait() run() returns expired, yield() or exception ─────────────────────────────────────────────────────────────────────SCHEDULING TRIGGERS: → Time Quantum Expiry: Thread runs for its allocated time slice (typically 1-10ms), then preempted → Voluntary Yield: Thread calls yield() or sleep() to give up remaining time slice → Priority Preemption: Higher-priority thread becomes runnable, may preempt lower-priority running thread → Blocking Operation: Thread enters BLOCKED/WAITING state, immediately context-switches to next threadOn a single-core CPU, threads don't actually run in parallel—they rapidly switch, creating the illusion of simultaneous execution. Even on multi-core systems, if you have more runnable threads than cores, not all threads run at once. The scheduler time-slices among them to create apparent concurrency.
Threads don't spend their entire lifecycle executing. Much of the time, threads are waiting—for locks, for I/O, for other threads, or for time to pass. Understanding these waiting states is crucial for diagnosing performance issues and deadlocks.
| State | Entered By | Exited By | Key Characteristic |
|---|---|---|---|
| BLOCKED | Attempting to enter synchronized block/method when lock is held by another thread | Lock becomes available and this thread acquires it | Waiting for monitor lock only; cannot be interrupted from blocking on lock |
| WAITING | Object.wait(), Thread.join(), LockSupport.park() | Object.notify(), notifyAll(), thread completes (join), unpark() | Indefinite wait; requires explicit wake-up signal |
| TIMED_WAITING | Thread.sleep(ms), Object.wait(ms), Thread.join(ms), LockSupport.parkNanos() | Timeout expires OR notify/unpark/join-thread-completes | Has a deadline; will eventually return to RUNNABLE |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
/** * Demonstrating different ways threads enter waiting states */import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';import { setTimeout } from 'timers/promises'; // ============================================// TIMED_WAITING: Sleep for a specified duration// ============================================async function timedWaitingExample(): Promise<void> { console.log('[TIMED_WAITING] Starting sleep...'); // Thread enters TIMED_WAITING state // Will automatically return to RUNNABLE after 2000ms await setTimeout(2000); console.log('[TIMED_WAITING] Woke up after 2 seconds');} // ============================================// WAITING: Wait for an event (simulated with Promise)// ============================================async function waitingExample(signal: AbortSignal): Promise<void> { console.log('[WAITING] Waiting for signal...'); return new Promise((resolve, reject) => { // Thread conceptually WAITING indefinitely // Will only proceed when abort signal fires signal.addEventListener('abort', () => { console.log('[WAITING] Received signal, resuming'); resolve(); }); });} // ============================================// BLOCKED: Waiting for exclusive resource (mutex simulation)// ============================================class MutualExclusionExample { private locked = false; private queue: Array<() => void> = []; async acquire(): Promise<void> { if (this.locked) { // Thread enters BLOCKED state conceptually // Waiting for the lock to be released console.log('[BLOCKED] Lock is held, waiting...'); await new Promise<void>((resolve) => { this.queue.push(resolve); }); console.log('[BLOCKED] Lock acquired after waiting'); } this.locked = true; console.log('[BLOCKED] Lock acquired immediately'); } release(): void { if (this.queue.length > 0) { // Wake up next waiting thread const next = this.queue.shift()!; next(); // BLOCKED → RUNNABLE transition } else { this.locked = false; } console.log('[BLOCKED] Lock released'); }} // ============================================// Example: Thread join (waiting for another thread)// ============================================if (isMainThread) { async function joinExample(): Promise<void> { console.log('[JOIN] Main thread creating worker...'); const worker = new Worker(` const { parentPort } = require('worker_threads'); console.log('[JOIN] Worker thread starting work...'); // Simulate work let sum = 0; for (let i = 0; i < 1e8; i++) { sum += i; } console.log('[JOIN] Worker thread completed'); parentPort.postMessage({ result: sum }); `, { eval: true }); // Main thread enters WAITING state // Will resume when worker thread completes await new Promise<void>((resolve) => { worker.on('message', (msg) => { console.log('[JOIN] Worker result:', msg.result); resolve(); }); }); console.log('[JOIN] Main thread resumed after worker completed'); }} // ============================================// Visualizing state transitions during execution// ============================================/*Timeline of thread states: Time │ Thread Action │ State Transition─────┼──────────────────────────────────┼──────────────────────t0 │ Thread created │ NEWt1 │ thread.start() called │ NEW → RUNNABLEt2 │ Scheduler picks thread │ RUNNABLE → RUNNINGt3 │ Thread calls sleep(1000) │ RUNNING → TIMED_WAITINGt4 │ 1000ms elapsed │ TIMED_WAITING → RUNNABLEt5 │ Scheduler picks thread │ RUNNABLE → RUNNINGt6 │ Thread tries to enter sync block │ RUNNING → BLOCKED │ (if lock held by other)t7 │ Lock released by other thread │ BLOCKED → RUNNABLEt8 │ Thread calls wait() │ RUNNING → WAITINGt9 │ notify() called by other thread │ WAITING → RUNNABLEt10 │ run() method returns │ RUNNING → TERMINATED*/BLOCKED threads are waiting to acquire a lock—they will automatically proceed once the lock is available. WAITING threads are waiting for an explicit signal—notify(), interrupt(), or similar. If no signal ever comes, WAITING threads wait forever. This distinction matters for debugging: BLOCKED threads indicate contention; WAITING threads may indicate missing notifications (potential deadlock).
A thread reaches the TERMINATED state when its execution completes. This can happen in several ways:
Once terminated, a thread cannot be restarted. Calling start() on a terminated thread throws an exception.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
import { Worker, isMainThread, parentPort } from 'worker_threads'; /** * Pattern: Cooperative Cancellation * * Threads should check for cancellation requests and clean up gracefully, * rather than being forcefully terminated. */ // ============================================// Pattern 1: Using AbortController/AbortSignal// ============================================class CancellableTask { private abortController: AbortController; constructor() { this.abortController = new AbortController(); } async run(): Promise<void> { const signal = this.abortController.signal; try { while (!signal.aborted) { // Do a unit of work await this.doWorkUnit(); // Check cancellation between units if (signal.aborted) { console.log('Cancellation detected, cleaning up...'); break; } } } finally { // Always cleanup, whether cancelled or completed await this.cleanup(); } console.log('Task terminated gracefully'); } cancel(): void { console.log('Cancellation requested'); this.abortController.abort(); } private async doWorkUnit(): Promise<void> { // Simulate work await new Promise(resolve => setTimeout(resolve, 100)); } private async cleanup(): Promise<void> { console.log('Releasing resources...'); // Close files, connections, release locks }} // ============================================// Pattern 2: Volatile stop flag (conceptual)// ============================================class StoppableWorker { private shouldStop = false; // Would be volatile/atomic in real threading async run(): Promise<void> { console.log('Worker started'); let iterationCount = 0; while (!this.shouldStop) { iterationCount++; // Perform work await this.processItem(iterationCount); // Periodically check stop flag (already in while condition) } console.log(`Worker stopped after ${iterationCount} iterations`); } requestStop(): void { this.shouldStop = true; } private async processItem(id: number): Promise<void> { console.log(`Processing item ${id}`); await new Promise(r => setTimeout(r, 200)); }} // ============================================// Pattern 3: Worker thread with message-based shutdown// ============================================if (isMainThread) { async function runWithGracefulShutdown(): Promise<void> { const worker = new Worker(` const { parentPort } = require('worker_threads'); let running = true; let processedCount = 0; // Listen for shutdown command parentPort.on('message', (msg) => { if (msg.type === 'shutdown') { console.log('[Worker] Shutdown requested'); running = false; } }); async function main() { while (running) { // Process work processedCount++; await new Promise(r => setTimeout(r, 100)); // Report progress parentPort.postMessage({ type: 'progress', count: processedCount }); } // Cleanup and report final state console.log('[Worker] Cleaning up...'); parentPort.postMessage({ type: 'shutdown_complete', finalCount: processedCount }); } main(); `, { eval: true }); // Let worker run for 1 second await new Promise(r => setTimeout(r, 1000)); // Request graceful shutdown worker.postMessage({ type: 'shutdown' }); // Wait for shutdown confirmation await new Promise<void>((resolve) => { worker.on('message', (msg) => { if (msg.type === 'shutdown_complete') { console.log(`[Main] Worker completed: ${msg.finalCount} items`); resolve(); } }); }); await worker.terminate(); }}Threads should terminate themselves in response to requests, not be terminated externally. This allows cleanup code to run, resources to be released, and invariants to be maintained. Design your threads with cancellation checkpoints and cleanup logic from the start.
A crucial aspect of thread lifecycle management is coordination—knowing when threads have finished their work and collecting their results. The primary mechanism for this is the join operation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
import { Worker, isMainThread } from 'worker_threads'; /** * Thread Join Operations * * join() causes the calling thread to wait until the target thread terminates. * This is essential for: * - Ensuring work is complete before proceeding * - Collecting results from worker threads * - Proper resource cleanup */ // ============================================// Pattern 1: Sequential Join (wait for each)// ============================================async function sequentialJoin(): Promise<void> { const workers: Worker[] = []; const results: number[] = []; // Create multiple workers for (let i = 0; i < 4; i++) { const worker = new Worker(/* worker code */, { workerData: i }); workers.push(worker); } // Wait for each worker sequentially // Total time = sum of all worker times for (const worker of workers) { const result = await new Promise<number>((resolve) => { worker.on('message', resolve); }); results.push(result); await worker.terminate(); } console.log('All workers complete:', results);} // ============================================// Pattern 2: Parallel Join (wait for all)// ============================================async function parallelJoin(): Promise<void> { const workerPromises: Promise<number>[] = []; // Create workers and capture their completion promises for (let i = 0; i < 4; i++) { const promise = new Promise<number>((resolve, reject) => { const worker = new Worker(/* worker code */, { workerData: i }); worker.on('message', (result) => { worker.terminate(); resolve(result); }); worker.on('error', reject); }); workerPromises.push(promise); } // Wait for ALL workers concurrently // Total time = max of all worker times (not sum) const results = await Promise.all(workerPromises); console.log('All workers complete:', results);} // ============================================// Pattern 3: Join with Timeout// ============================================async function joinWithTimeout( worker: Worker, timeoutMs: number): Promise<{ success: boolean; result?: unknown }> { return new Promise((resolve) => { const timer = setTimeout(() => { console.log('Join timed out, terminating worker'); worker.terminate(); resolve({ success: false }); }, timeoutMs); worker.on('message', (result) => { clearTimeout(timer); worker.terminate(); resolve({ success: true, result }); }); worker.on('error', (err) => { clearTimeout(timer); worker.terminate(); resolve({ success: false }); }); });} // ============================================// Pattern 4: Race - First Completion Wins// ============================================async function raceWorkers(): Promise<unknown> { const workers: Worker[] = []; // Start multiple workers doing the same task for (let i = 0; i < 3; i++) { const worker = new Worker(/* worker code */, { workerData: i }); workers.push(worker); } // Wait for FIRST worker to complete const result = await Promise.race( workers.map((worker, index) => new Promise((resolve) => { worker.on('message', (result) => { resolve({ workerIndex: index, result }); }); }) ) ); // Terminate remaining workers for (const worker of workers) { worker.terminate(); } console.log('First worker won:', result); return result;} // ============================================// Lifecycle diagram with join// ============================================/*Main Thread Worker Thread 1 Worker Thread 2 │ │ │ │ create │ │ ├────────────────────►│ │ │ │ [RUNNABLE] │ │ create │ │ ├────────────────────────────────────────────► │ │ │ [RUNNABLE] │ │ │ │ join(worker1) │ │ │ [WAITING] │ (executing) │ (executing) │ · │ │ │ · │ [TERMINATED] │ ├─────────────────────┘ │ │ [RUNNABLE] │ │ │ │ join(worker2) │ │ [WAITING] │ (executing) │ · │ │ · │ [TERMINATED] ├────────────────────────────────────────────┘ │ [RUNNABLE] │ │ (continue processing) ▼*/After a successful join(), the thread is guaranteed to be TERMINATED. All writes made by the terminated thread are visible to the joining thread—this is called the 'join happens-before relationship.' This memory visibility guarantee is crucial for safely reading results computed by worker threads.
Threads are classified into two categories based on their relationship to process lifetime: user threads (foreground) and daemon threads (background).
| Aspect | User Thread (Foreground) | Daemon Thread (Background) |
|---|---|---|
| Default | Yes - threads are user threads by default | Must explicitly set daemon=true before start() |
| Process lifetime | JVM/process waits for all user threads to complete | JVM/process can exit even if daemon threads are running |
| Exit behavior | Thread must complete or be explicitly terminated | Abruptly stopped when the last user thread exits |
| Cleanup | Finally blocks execute; resources can be released | Finally blocks may NOT execute; resources may leak |
| Use cases | Main application logic, critical operations | Background services, monitoring, GC, housekeeping |
| Examples | Request handlers, business logic, main thread | GC threads, timer threads, monitoring agents |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
import java.util.concurrent.*; public class DaemonThreadDemo { public static void main(String[] args) throws InterruptedException { // Create a daemon thread for background work Thread daemonThread = new Thread(() -> { try { while (true) { System.out.println("[Daemon] Background cleanup running..."); Thread.sleep(500); // Perform periodic maintenance performBackgroundCleanup(); } } finally { // WARNING: This finally block may never execute! // Daemon threads are killed abruptly when JVM exits System.out.println("[Daemon] Cleanup - THIS MAY NOT PRINT!"); } }); // MUST set daemon before start() daemonThread.setDaemon(true); daemonThread.start(); // Create user (foreground) thread for main work Thread userThread = new Thread(() -> { try { System.out.println("[User] Starting main work..."); Thread.sleep(2000); System.out.println("[User] Main work complete"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // This finally block WILL execute System.out.println("[User] Cleanup completed"); } }); userThread.start(); // Main thread waits for user thread userThread.join(); System.out.println("[Main] User thread done, exiting..."); // At this point, main thread is the only remaining user thread // When main() returns, JVM will: // 1. Wait for other user threads (none remaining) // 2. Terminate daemon threads abruptly // 3. Exit the process } private static void performBackgroundCleanup() { // Log rotation, temp file cleanup, cache expiration, etc. }} /*Output:[Daemon] Background cleanup running...[User] Starting main work...[Daemon] Background cleanup running...[Daemon] Background cleanup running...[Daemon] Background cleanup running...[User] Main work complete[User] Cleanup completed[Main] User thread done, exiting... Note: "[Daemon] Cleanup - THIS MAY NOT PRINT!" never appears!The daemon thread was killed mid-execution.*/Never use daemon threads for operations that must complete (like writing files, sending network data, or updating databases). Daemon threads can be terminated at any point in their execution—even in the middle of a write operation—leaving data corrupted or transactions incomplete.
We've thoroughly explored the complete lifecycle of a thread—every state, transition, and management pattern. Let's consolidate the essential knowledge:
What's Next:
With thread lifecycle mastered, we're ready to explore the practical skills of creating and managing threads. We'll examine thread pools, executor services, thread factories, and thread management patterns that are used in production systems.
You now understand the complete lifecycle of threads—how they're created, scheduled, how they wait, and how they terminate. This lifecycle knowledge is essential for debugging concurrency issues, managing resources, and designing robust concurrent applications.