13.1 Thread Fundamentals, `Runnable`, `Callable`, and Thread Lifecycle
Key Takeaways
Java platform threads map 1:1 to underlying operating system kernel threads; thread execution order and timeslicing are nondeterministically governed by the OS scheduler.
The Runnable functional interface defines 'void run()' and cannot throw checked exceptions, whereas
Callable<V>defines 'V call() throws Exception' and returns a typed result.Invoking start() transitions a thread from NEW to RUNNABLE and spawns a new OS execution thread, whereas invoking run() directly executes synchronously on the caller's thread without starting a new thread.
Calling start() more than once on the same Thread instance throws IllegalThreadStateException, as does calling setDaemon(true) after start() has been invoked.
The static Thread.interrupted() method returns the interrupt status of the current thread and clears it, while the instance method isInterrupted() queries the interrupt status without clearing the flag.
Thread Fundamentals, Runnable, Callable, and Thread Lifecycle
Concurrency enables multiple execution sequences to proceed simultaneously or interleaved across modern multi-core processors. In Java SE 17, understanding the boundary between operating system execution threads and Java runtime thread abstractions is fundamental. Mastering thread creation patterns, the exact state transitions of the thread lifecycle, daemon thread semantics, and thread coordination mechanics is essential for developing robust multi-threaded applications.
Operating System Threads vs. Java Platform Threads
In standard Java Virtual Machine (JVM) implementations, each instance of java.lang.Thread is a platform thread that wraps a 1:1 underlying operating system (OS) kernel thread.
+-------------------------------------------------------------+
| Java Application (JVM User Space) |
| Thread A (Stack ~1MB) Thread B (Stack ~1MB) |
+-------------------------------------------------------------+
| |
v (1:1 Native Mapping) v (1:1 Native Mapping)
+-------------------------------------------------------------+
| Operating System Kernel |
| Kernel Thread 1042 Kernel Thread 1043 |
| [Preemptive OS Scheduler / CPU Core Time-Slicing] |
+-------------------------------------------------------------+
Because Java platform threads rely directly on native OS threads:
- Resource Footprint: Each platform thread allocates a substantial memory footprint for its private call stack (typically 1 megabyte by default, configurable via the
-XssJVM flag). - Scheduling Mechanics: Thread scheduling is preemptive and platform-dependent. The underlying operating system scheduler decides which thread receives a CPU time slice (quantum) and when context switching occurs. Java code cannot guarantee execution ordering or execution velocity.
- Thread Priorities: Java provides advisory thread priority levels from
Thread.MIN_PRIORITY(1) toThread.MAX_PRIORITY(10), defaulting toThread.NORM_PRIORITY(5). However, operating systems map these values arbitrarily to native priority levels or ignore them altogether. Never rely on thread priorities for algorithmic correctness.
Creating Threads: Thread, Runnable, and Callable<V>
Java provides three primary mechanisms for expressing asynchronous or concurrent units of execution.
1. Extending java.lang.Thread
A class can extend Thread and override its public void run() method:
public class WorkerThread extends Thread {
@Override
public void run() {
System.out.println("Worker executing in: " + Thread.currentThread().getName());
}
}
// Instantiation and startup:
WorkerThread worker = new WorkerThread();
worker.start();
Extending Thread tightly couples the execution mechanism to the task logic and consumes the single class inheritance allowance in Java, making it the least flexible approach.
2. Implementing java.lang.Runnable
The Runnable interface represents an abstract task decoupled from thread management:
@FunctionalInterface
public interface Runnable {
void run();
}
Because Runnable is a functional interface, it can be expressed concisely using lambda expressions or method references:
Runnable task = () -> System.out.println("Task running in: " + Thread.currentThread().getName());
Thread thread = new Thread(task, "CustomWorkerThread");
thread.start();
Key constraints of Runnable:
- The
run()method returnsvoid(it cannot return a computed result directly to the caller). - The
run()method signature does not declare any checked exceptions; any checked exception must be caught and handled within the method body.
3. Implementing java.util.concurrent.Callable<V>
Introduced in java.util.concurrent, Callable<V> overcomes the limitations of Runnable:
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable<V> returns a parameterized type V and can throw checked Exception instances. However, Thread does not possess a constructor accepting a Callable. Callable instances are submitted to an ExecutorService, which coordinates execution and returns a Future<V>.
| Feature | Thread subclass | Runnable | Callable<V> |
|---|---|---|---|
| Mechanism | Class inheritance | Functional interface | Functional interface |
| Return Type | void | void | Generic type V |
| Checked Exceptions | Cannot throw | Cannot throw | Can throw Exception |
| Execution Target | Self-managed | Passed to Thread or Executor | Submitted to ExecutorService |
Thread Lifecycle and the Thread.State Enum
A Java thread resides in exactly one state at any given point in time, as defined by Thread.State:
[NEW] ---(start())---> [RUNNABLE] <-----------------------+
| |
+-------------------+--------------------+ |
| | | |
v v v |
[BLOCKED] [WAITING] [TIMED_WAITING] |
(waiting for (wait(), join(), (sleep(), wait(t)|
intrinsic monitor) park()) join(t)) |
| | | |
+-------------------+--------------------+---------+
|
v (run() completes)
[TERMINATED]
NEW: The thread instance has been created (new Thread(...)), butstart()has not yet been invoked.RUNNABLE: The thread is executing in the JVM or is ready and waiting for resource allocation (CPU time slice) from the operating system scheduler.BLOCKED: The thread is waiting to acquire an intrinsic monitor lock to enter asynchronizedblock or method, or to re-enter asynchronizedblock afterObject.wait().WAITING: The thread is waiting indefinitely for another thread to perform a specific action without any timeout:Object.wait(),Thread.join(), orLockSupport.park().TIMED_WAITING: The thread is waiting for another thread to perform an action up to a specified waiting time:Thread.sleep(millis),Object.wait(timeout),Thread.join(timeout), orLockSupport.parkNanos().TERMINATED: The thread has finished execution because itsrun()method completed normally or terminated abruptly due to an uncaught exception.
Warning
The IllegalThreadStateException Trap
Once a thread transitions out of NEW by invoking start(), it can never be started again. Calling start() on an already started thread—even if it is currently in RUNNABLE, WAITING, or has already reached TERMINATED—immediately throws an IllegalThreadStateException.
The start() vs. run() Execution Trap
A frequent source of bugs and exam traps is the distinction between calling start() and calling run() directly:
Thread thread = new Thread(() -> {
System.out.println("Executing thread: " + Thread.currentThread().getName());
});
thread.run(); // Executes SYNCHRONOUSLY on the CALLER thread (e.g., 'main')!
thread.start(); // Spawns a NEW native thread ('Thread-0') and executes asynchronously!
- Calling
run()directly: Invokes therun()method as an ordinary method call on the current thread. No new native thread is created, no OS scheduler registration occurs, and the caller blocks untilrun()returns. - Calling
start(): Requests the JVM to allocate a native thread stack, registers the thread with the OS scheduler, transitions the thread state toRUNNABLE, and asynchronously invokes therun()method on the newly created thread.
Daemon Threads and JVM Termination Lifecycle
Java threads are divided into two categories:
- User Threads (Non-Daemon): High-priority threads that execute core application logic. The JVM remains active as long as at least one non-daemon thread is alive.
- Daemon Threads: Background service providers (such as garbage collection or internal monitoring). They do not prevent the JVM from exiting.
Thread daemonWorker = new Thread(() -> {
try {
while (true) {
Thread.sleep(1000);
System.out.println("Heartbeat...");
}
} finally {
System.out.println("Daemon finally block executing..."); // MAY NEVER RUN!
}
});
daemonWorker.setDaemon(true); // MUST be called BEFORE start()!
daemonWorker.start();
Critical Daemon Thread Rules
- State Configuration Order:
setDaemon(true)must be called beforestart(). InvokingsetDaemon()on an active thread throwsIllegalThreadStateException. - Inheritance: A newly created thread inherits the daemon status of the thread that created it.
- Abrupt Termination: When all user (non-daemon) threads terminate, the JVM immediately terminates. Any running daemon threads are halted abruptly: their
finallyblocks do not execute, and resources held by them may not be released cleanly.
Thread Interruption and Coordination: interrupt(), join(), and sleep()
Java relies on a cooperative interruption policy. One thread cannot forcefully terminate another thread; it can only request that the target thread interrupt its current activity.
Interruption Mechanics and API Methods
Each thread maintains an internal boolean interrupt status flag:
t.interrupt(): Sets the target thread's interrupt status flag totrue. If the target thread is currently blocked inThread.sleep(),Object.wait(), orThread.join(), the blocking call throwsInterruptedExceptionand clears the interrupt status flag back tofalse.t.isInterrupted(): Instance method that queries the target thread's interrupt status, returningtrueorfalse. It does not alter the interrupt flag.Thread.interrupted(): Static method that queries the current executing thread's interrupt status and clears the flag (resets it tofalse).
public class InterruptibleTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// Simulating work
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Caught InterruptedException! Interrupt flag is now: "
+ Thread.currentThread().isInterrupted()); // Prints: false!
// Best practice: restore interrupted status or exit
Thread.currentThread().interrupt();
break;
}
}
}
}
Note
Notice the static vs. instance method distinction: Thread.interrupted() always queries and clears the status of the thread invoking the method, regardless of which Thread reference it is called on. In contrast, t.isInterrupted() inspects thread t without clearing its state.
Thread Coordination with join() and sleep()
Thread.sleep(long millis): Suspends the currently executing thread for at least the specified millisecond duration. The thread transitions toTIMED_WAITING. Crucially,Thread.sleep()does not release any held locks or monitors.t.join(): Causes the current thread to pause and transition toWAITINGuntil the target threadtterminates. An overloaded variantt.join(long millis)transitions the caller toTIMED_WAITING. Both variants throwInterruptedExceptionif the calling thread is interrupted while waiting.
Consider the following code snippet executing in a Java application:
Thread worker = new Thread(() -> {
Thread.currentThread().interrupt();
System.out.print(Thread.interrupted() + " ");
System.out.print(Thread.currentThread().isInterrupted() + " ");
System.out.print(Thread.interrupted() + " ");
});
worker.start();
worker.join();
What is the printed output?
true true false
true false false
false false false
true true true
Given the following class definition:
public class ThreadLauncher {
public static void main(String[] args) {
Thread task = new Thread(() -> System.out.print("WORK "));
task.run();
task.start();
task.start();
}
}
What is the result of executing this code?
It throws IllegalThreadStateException immediately on task.run().
It prints 'WORK ' and then throws IllegalStateException on task.start().
It prints 'WORK WORK WORK ' without errors.
WORK is printed twice (once by run(), once by the started thread) and the second start() throws IllegalThreadStateException.
A developer writes the following program to process background metrics:
public class BackgroundReporter {
public static void main(String[] args) throws Exception {
Thread monitor = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.print("Report-Saved ");
} catch (InterruptedException e) {
System.out.print("Interrupted ");
} finally {
System.out.print("Cleaned ");
}
});
monitor.setDaemon(true);
monitor.start();
System.out.print("Main-Done ");
}
}
Assuming the main thread completes execution in less than 50 milliseconds, what is the expected outcome?
The JVM prints 'Main-Done Interrupted Cleaned ' immediately as monitor is interrupted upon exit.
A compilation error occurs because setDaemon(true) cannot be called on a Thread that sleeps.
The JVM prints 'Main-Done ' and exits immediately; neither 'Report-Saved ' nor 'Cleaned ' is guaranteed to execute.
The JVM waits 2000 milliseconds, printing 'Main-Done Report-Saved Cleaned '.
Sections you finish are checked off in the contents.