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.

Last updated: October 2026

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 -Xss JVM 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) to Thread.MAX_PRIORITY (10), defaulting to Thread.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 returns void (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>.

FeatureThread subclassRunnableCallable<V>
MechanismClass inheritanceFunctional interfaceFunctional interface
Return TypevoidvoidGeneric type V
Checked ExceptionsCannot throwCannot throwCan throw Exception
Execution TargetSelf-managedPassed to Thread or ExecutorSubmitted 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]
  1. NEW: The thread instance has been created (new Thread(...)), but start() has not yet been invoked.
  2. RUNNABLE: The thread is executing in the JVM or is ready and waiting for resource allocation (CPU time slice) from the operating system scheduler.
  3. BLOCKED: The thread is waiting to acquire an intrinsic monitor lock to enter a synchronized block or method, or to re-enter a synchronized block after Object.wait().
  4. WAITING: The thread is waiting indefinitely for another thread to perform a specific action without any timeout: Object.wait(), Thread.join(), or LockSupport.park().
  5. 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), or LockSupport.parkNanos().
  6. TERMINATED: The thread has finished execution because its run() 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 the run() 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 until run() returns.
  • Calling start(): Requests the JVM to allocate a native thread stack, registers the thread with the OS scheduler, transitions the thread state to RUNNABLE, and asynchronously invokes the run() method on the newly created thread.

Daemon Threads and JVM Termination Lifecycle

Java threads are divided into two categories:

  1. 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.
  2. 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 before start(). Invoking setDaemon() on an active thread throws IllegalThreadStateException.
  • 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 finally blocks 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 to true. If the target thread is currently blocked in Thread.sleep(), Object.wait(), or Thread.join(), the blocking call throws InterruptedException and clears the interrupt status flag back to false.
  • t.isInterrupted(): Instance method that queries the target thread's interrupt status, returning true or false. 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 to false).
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 to TIMED_WAITING. Crucially, Thread.sleep() does not release any held locks or monitors.
  • t.join(): Causes the current thread to pause and transition to WAITING until the target thread t terminates. An overloaded variant t.join(long millis) transitions the caller to TIMED_WAITING. Both variants throw InterruptedException if the calling thread is interrupted while waiting.
Test Your Knowledge

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?

A

true true false

B

true false false

C

false false false

D

true true true

Test Your Knowledge

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?

A

It throws IllegalThreadStateException immediately on task.run().

B

It prints 'WORK ' and then throws IllegalStateException on task.start().

C

It prints 'WORK WORK WORK ' without errors.

D

WORK is printed twice (once by run(), once by the started thread) and the second start() throws IllegalThreadStateException.

Test Your Knowledge

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?

A

The JVM prints 'Main-Done Interrupted Cleaned ' immediately as monitor is interrupted upon exit.

B

A compilation error occurs because setDaemon(true) cannot be called on a Thread that sleeps.

C

The JVM prints 'Main-Done ' and exits immediately; neither 'Report-Saved ' nor 'Cleaned ' is guaranteed to execute.

D

The JVM waits 2000 milliseconds, printing 'Main-Done Report-Saved Cleaned '.

Sections you finish are checked off in the contents.