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
// ── 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│
// ▼
// TERMINATEDThread Properties — Name, Priority, Daemon Status, and Uncaught Exception Handler
// ── 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
// ── 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 → volatile → synchronized (monitor unlock/lock)
// Violating it: any unsynchronized access to shared mutable state