☕ Java

Thread Basics

A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.

Thread Lifecycle — States and Transitions

A Java thread moves through six states defined in the Thread.State enum: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. These states are Java-level abstractions; the OS may have its own finer-grained states beneath, but the JVM presents this six-state model via Thread.getState(). NEW is the state of a Thread object that has been constructed but not yet started. The thread object exists as a Java object on the heap, but no OS thread has been created yet. Calling start() on a NEW thread allocates the OS thread, allocates the thread's stack, and transitions the thread to RUNNABLE. RUNNABLE covers both threads that are actually executing on a CPU core and threads that are ready to execute and waiting for the OS scheduler to assign them a core. The JVM cannot distinguish between these sub-states because it depends on the OS scheduler's decision, which can change at any instruction. A thread in RUNNABLE may be executing, may have been preempted by the OS, or may be waiting for I/O in the OS kernel — from the JVM's perspective, all of these are RUNNABLE. BLOCKED is the state of a thread waiting to acquire a monitor lock (the synchronized keyword). When two threads contend for the same synchronized block or method, the losing thread transitions to BLOCKED and waits until the winning thread exits the synchronized region and releases the monitor. BLOCKED is specifically and exclusively about monitor acquisition — no other form of waiting produces the BLOCKED state. WAITING is the state of a thread that has given up the CPU to wait indefinitely for a specific event. Object.wait() (which must be called while holding the object's monitor), Thread.join() with no timeout, and LockSupport.park() all produce the WAITING state. A thread in WAITING will not transition to RUNNABLE until another thread explicitly wakes it: Object.notify()/notifyAll(), the awaited thread terminating (for join()), or LockSupport.unpark(). TIMED_WAITING is identical to WAITING except that the thread supplies a timeout and will automatically transition back to RUNNABLE when the timeout expires: Thread.sleep(ms), Object.wait(timeout), Thread.join(timeout), and LockSupport.parkNanos() all produce TIMED_WAITING. TERMINATED is the state after a thread's run() method has returned normally, thrown an uncaught exception, or the thread was stopped. A TERMINATED thread cannot be restarted — calling start() on a terminated thread throws IllegalThreadStateException.
Java
// ── All six Thread.State values and what causes each ─────────────────
import java.util.concurrent.locks.LockSupport;

Object monitor = new Object();

// NEW: constructed but not started
Thread t = new Thread(() -> {});
System.out.println(t.getState());   // NEW

// RUNNABLE: after start(), actively runnable or executing
t.start();
System.out.println(t.getState());   // RUNNABLE (or TERMINATED if it ran already)

// ── BLOCKED: waiting to acquire a monitor ────────────────────────────
Object lock = new Object();

Thread blocker = new Thread(() -> {
    synchronized (lock) {
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
    }
});
Thread blocked = new Thread(() -> {
    synchronized (lock) {}  // waits for blocker to release lock
});

blocker.start();
Thread.sleep(50);     // let blocker acquire the lock first
blocked.start();
Thread.sleep(50);     // let blocked try to acquire

System.out.println(blocked.getState());  // BLOCKED
blocker.interrupt();
blocked.join(); blocker.join();

// ── WAITING: indefinite wait for notification ─────────────────────────
Object notifyTarget = new Object();
Thread waiter = new Thread(() -> {
    synchronized (notifyTarget) {
        try { notifyTarget.wait(); } catch (InterruptedException e) {}
    }
});
waiter.start();
Thread.sleep(50);
System.out.println(waiter.getState());  // WAITING
synchronized (notifyTarget) { notifyTarget.notify(); }
waiter.join();

// ── TIMED_WAITING: wait with a deadline ──────────────────────────────
Thread sleeper = new Thread(() -> {
    try { Thread.sleep(2000); } catch (InterruptedException e) {}
});
sleeper.start();
Thread.sleep(50);
System.out.println(sleeper.getState());  // TIMED_WAITING
sleeper.interrupt();
sleeper.join();

// ── TERMINATED: run() returned or threw ──────────────────────────────
Thread shortLived = new Thread(() -> System.out.println("done"));
shortLived.start();
shortLived.join();
System.out.println(shortLived.getState());   // TERMINATED

// Attempting to restart a terminated thread:
try {
    shortLived.start();   // IllegalThreadStateException — cannot restart
} catch (IllegalThreadStateException e) {
    System.out.println("Cannot restart: " + e);
}

// ── Complete state transition diagram (as comments) ───────────────────
// NEW ──start()──▶ RUNNABLE ──scheduled──▶ [executing]
//                     ▲                        │
//                     │              synchronized block contended
//                     │                        │
//                   notify()              BLOCKED ─────────────────┐
//                   join() completes           │ lock acquired      │
//                   unpark()                   └──────────▶ RUNNABLE│
//                     │                                            │
//                     │         wait()/join()/park()               │
//                     └──────── WAITING ◀──────────────────────────┤
//                     │                                            │
//                     │         sleep()/wait(t)/join(t)/parkNanos()│
//                     └──────── TIMED_WAITING ◀────────────────────┘
//                                                                  │
//                                              run() returns/throws
//                                                                  ▼
//                                                           TERMINATED

Thread Properties — Name, Priority, Daemon Status, and Uncaught Exception Handler

Every thread has a name, which defaults to "Thread-N" for an auto-generated integer N. Names serve no functional purpose at the JVM level but are invaluable for debugging: thread dumps, profilers, monitoring tools, and log frameworks all display thread names. Giving threads meaningful names — "payment-processor-1", "db-connection-pool-3", "order-event-consumer" — makes diagnosing deadlocks, thread leaks, and performance issues dramatically faster. The name is set at construction time (new Thread(runnable, "my-name")) or via setName(String). Thread priority is an integer from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10) with Thread.NORM_PRIORITY (5) as the default. Priority is a hint to the OS scheduler: higher-priority threads are more likely to be scheduled ahead of lower-priority threads when CPU time is scarce. The OS is free to ignore priorities, and different operating systems implement priority hints differently — on Linux, JVM thread priorities map to nice values or are ignored entirely by the CFS scheduler. Priority should never be used as a correctness mechanism; it is purely a scheduling hint for performance tuning in specific, profiled scenarios. Code that requires a thread to run before another for correctness must use synchronization, not priority. Daemon threads are background threads that the JVM will not wait for when shutting down. The JVM exits when all non-daemon threads have terminated, regardless of how many daemon threads are still running. Daemon threads are appropriate for background tasks that serve other threads — garbage collection (the GC threads are daemon threads), cache maintenance, metrics collection, keep-alive pings, and other support functions that should not prevent the application from exiting. Setting daemon status must happen before start() is called; calling setDaemon() on a running thread throws IllegalThreadStateException. A thread inherits its daemon status from the thread that created it: threads created by daemon threads are daemon threads by default. The uncaught exception handler is invoked when a thread's run() method throws an uncaught exception and the thread is about to terminate. Without a handler, the JVM prints the exception stack trace to System.err and the thread dies silently. Setting a thread-level handler (thread.setUncaughtExceptionHandler(...)) or a default handler (Thread.setDefaultUncaughtExceptionHandler(...)) allows applications to log exceptions to a structured logging system, emit metrics, trigger alerts, or restart threads. Thread pools set their own uncaught exception handler on the threads they create; understanding this handler chain is important when debugging exceptions that seem to disappear in executor-based code.
Java
// ── Thread naming — always name your threads ─────────────────────────
Thread named = new Thread(() -> {
    System.out.println("Running: " + Thread.currentThread().getName());
}, "order-processor-1");
named.start();
named.join();
// Output: Running: order-processor-1

// Renaming after construction (but before meaningful use):
Thread t = new Thread(() -> {});
t.setName("background-sync");
System.out.println(t.getName());  // background-sync

// In thread dumps, names distinguish threads:
// "order-processor-1" #23 prio=5 os_prio=0 cpu=1.23ms ...
//    java.lang.Thread.State: RUNNABLE

// ── Thread priority — hint, not guarantee ────────────────────────────
Thread high = new Thread(() -> { /* CPU-bound */ }, "high-priority");
Thread low  = new Thread(() -> { /* CPU-bound */ }, "low-priority");

high.setPriority(Thread.MAX_PRIORITY);  // 10
low.setPriority(Thread.MIN_PRIORITY);   // 1

// Checking bounds:
System.out.println(Thread.MIN_PRIORITY);   // 1
System.out.println(Thread.NORM_PRIORITY);  // 5
System.out.println(Thread.MAX_PRIORITY);   // 10

// Priority has NO guaranteed effect — do not use for correctness:
// high.start(); low.start();  // high may not actually run first

// ── Daemon threads and JVM shutdown ──────────────────────────────────
Thread daemonThread = new Thread(() -> {
    while (true) {
        System.out.println("Background tick...");
        try { Thread.sleep(500); } catch (InterruptedException e) { break; }
    }
}, "background-ticker");
daemonThread.setDaemon(true);   // MUST be called before start()
daemonThread.start();

// The JVM will not wait for daemonThread.
// When main() returns, if no non-daemon threads remain, the JVM exits
// and daemonThread is killed mid-execution — possibly mid-println.
Thread.sleep(1200);   // let it tick a few times
System.out.println("Main exiting — daemon thread will be killed");
// No daemonThread.join() needed or useful

// Verifying daemon inheritance:
Thread createdByDaemon = new Thread(() -> {});
// If this thread were started by a daemon thread, it would be a daemon too.
// From main thread (non-daemon), it inherits non-daemon status:
System.out.println(createdByDaemon.isDaemon());  // false

// ── Uncaught exception handler ────────────────────────────────────────
// Thread-level handler:
Thread failProne = new Thread(() -> {
    throw new RuntimeException("Something went wrong");
}, "risky-thread");

failProne.setUncaughtExceptionHandler((thread, ex) -> {
    System.err.printf("[%s] Uncaught exception: %s%n", thread.getName(), ex.getMessage());
    // In production: log to structured logger, emit metric, alert on-call
});
failProne.start();
failProne.join();
// Prints: [risky-thread] Uncaught exception: Something went wrong

// Default handler — catches uncaught exceptions from ALL threads without a specific handler:
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
    System.err.printf("[DEFAULT HANDLER] Thread '%s' died: %s%n",
        thread.getName(), ex.getClass().getSimpleName());
});

// Handler precedence:
// 1. Thread's own handler (setUncaughtExceptionHandler)
// 2. Thread's ThreadGroup handler (rarely overridden)
// 3. Default handler (Thread.setDefaultUncaughtExceptionHandler)
// 4. Print to System.err (JVM default if no handler set)

Interruption, join(), sleep(), and the Happens-Before Relationship

Thread interruption is Java's cooperative cancellation mechanism. Calling thread.interrupt() does not stop the thread; it sets the thread's interrupt flag to true and, if the thread is currently blocked in a method that is declared to throw InterruptedException (Thread.sleep(), Object.wait(), Thread.join(), and many methods in java.util.concurrent), it causes that method to throw InterruptedException immediately, clearing the interrupt flag. If the thread is not currently blocked in an interruptible method, the flag is set but nothing else happens — the thread continues running until it checks the flag itself via Thread.interrupted() (static, clears the flag) or Thread.currentThread().isInterrupted() (instance, does not clear the flag). The correct response to InterruptedException is to either propagate it (declare the method as throws InterruptedException) or, if the method cannot propagate it, restore the interrupt flag by calling Thread.currentThread().interrupt() before returning. Swallowing InterruptedException — catching it and doing nothing — destroys the cancellation signal and makes the thread impossible to interrupt or shut down cleanly. This is the most common and most dangerous misuse of the interruption mechanism. Thread.join() causes the calling thread to wait until the target thread terminates. join(long millis) waits at most the specified time. join() with no argument waits indefinitely. After a successful join(), the calling thread is guaranteed to see all memory writes performed by the target thread before it terminated — this is a happens-before relationship. The happens-before relationship is the Java Memory Model's formal guarantee about visibility. If action A happens-before action B, then all memory writes performed during or before A are visible to action B. The key happens-before rules: a thread's start() call happens-before any action in the started thread; every action in thread A happens-before thread B's successful join() on A; a write to a volatile variable happens-before every subsequent read of that variable; a monitor unlock happens-before every subsequent lock of the same monitor. Without a happens-before relationship between a write and a read, the Java Memory Model makes no guarantee that the read will see the write — the read might see a stale cached value, an incompletely initialized value, or values from a reordered write sequence.
Java
// ── Interruption — cooperative cancellation ───────────────────────────
Thread worker = new Thread(() -> {
    System.out.println("Worker started");
    try {
        while (!Thread.currentThread().isInterrupted()) {
            // Do one unit of work
            System.out.println("Working...");
            Thread.sleep(200);   // InterruptedException clears the flag and throws
        }
    } catch (InterruptedException e) {
        // sleep() threw — flag already cleared. We handle graceful shutdown:
        System.out.println("Worker interrupted during sleep — shutting down");
        // Do NOT swallow silently. Restore flag if caller needs to check it:
        Thread.currentThread().interrupt();  // restore flag
    }
    System.out.println("Worker finished cleanly");
}, "cancellable-worker");

worker.start();
Thread.sleep(550);       // let it run a few iterations
worker.interrupt();      // request cancellation
worker.join();           // wait for clean shutdown

// ── The two ways to check the interrupt flag ──────────────────────────
// Thread.interrupted()              — static, checks current thread, CLEARS the flag
// Thread.currentThread().isInterrupted() — instance, does NOT clear the flag

// Use isInterrupted() in loop conditions (non-destructive):
while (!Thread.currentThread().isInterrupted()) { /* ... */ }

// Use Thread.interrupted() only when you intentionally consume and clear the flag:
if (Thread.interrupted()) {
    throw new InterruptedException();   // propagate as checked exception
}

// ── The swallowing anti-pattern — never do this: ─────────────────────
Thread bad = new Thread(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        // WRONG: silently swallowing destroys the cancellation signal
        // The thread's interrupted flag is now clear; callers can't cancel it
    }
    // continues running as if nothing happened
});

// ── Thread.join() — waiting for another thread to finish ─────────────
Thread producer = new Thread(() -> {
    System.out.println("Producing...");
    try { Thread.sleep(300); } catch (InterruptedException e) {}
    System.out.println("Production complete");
}, "producer");

producer.start();
producer.join();  // main thread waits here until producer finishes
System.out.println("Main: producer is done, safe to read its output");
// join() establishes happens-before: everything producer wrote is visible here

// join with timeout:
Thread slow = new Thread(() -> {
    try { Thread.sleep(5000); } catch (InterruptedException e) {}
}, "slow-thread");
slow.start();
slow.join(1000);  // wait at most 1 second
if (slow.isAlive()) {
    System.out.println("Slow thread still running after 1s — interrupting");
    slow.interrupt();
}

// ── Happens-before — visibility guarantee examples ────────────────────
// Without happens-before: a read may see a stale value
class VisibilityRisk {
    boolean ready = false;  // not volatile — no happens-before guarantee
    int value = 0;

    void writer() {
        value = 42;
        ready = true;   // no guarantee: reader might see ready=true, value=0
    }
    void reader() {
        while (!ready) {}   // might loop forever OR see ready=true then value=0
        System.out.println(value);  // might print 0, not 42
    }
}

// With volatile: happens-before is established
class VisibilitySafe {
    volatile boolean ready = false;  // volatile write happens-before volatile read
    int value = 0;

    void writer() {
        value = 42;         // write value before volatile write
        ready = true;       // volatile write — publishes value=42
    }
    void reader() {
        while (!ready) {}   // volatile read — sees ready=true only after value=42
        System.out.println(value);  // guaranteed to print 42
    }
}

// Happens-before chain: start → join → volatilesynchronized (monitor unlock/lock)
// Violating it: any unsynchronized access to shared mutable state

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.
Thread Priority
Thread priority in Java is an integer hint to the OS scheduler indicating the relative importance of a thread compared to others. Java defines a scale from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10) with Thread.NORM_PRIORITY (5) as the default, and every thread inherits the priority of the thread that created it. The critical word in this definition is hint: thread priority is advisory, not mandatory. The JVM maps Java priorities to native OS thread priorities, and the OS scheduler uses those priorities according to its own scheduling policy, which varies by operating system, scheduler configuration, and system load. On some platforms, priority has a measurable effect on scheduling frequency; on others, it is almost entirely ignored. Priority must never be used as a correctness mechanism — any program that requires a thread to run before another for correctness, rather than merely preferring it, is broken and will fail on any platform where priorities are not honored. This entry covers the priority scale, inheritance, platform mapping, the correctness prohibition, starvation, priority inversion, and the narrow set of cases where priority hints are legitimately useful.