Java Thread Creation & Lifecycle Explained from Scratch
Multi-threading is a core pillar of concurrent programming in Java, enabling applications to perform multiple tasks simultaneously. This guide covers how to create threads, the differences between execution strategies, the underlying operating system mechanics, and the complete state transitions of the Java thread lifecycle.
1. Why Do We Need Threads? (The Problem of Single-Threaded Execution)
By default, when a Java program starts, the Java Virtual Machine (JVM) launches a single thread called the main thread to execute the main() method. In a purely single-threaded environment, execution is sequential. If a task takes a long time to complete (such as a heavy database query, file I/O, or network request), the entire application freezes, and subsequent tasks must wait.
To prevent blocking and to utilize modern multi-core processors, we use multi-threading. This allows us to spin up independent paths of execution (threads) that run concurrently within the same process space, sharing the same memory heap but maintaining their own execution stacks.
2. Thread Creation Method 1: Extending the Thread Class
The first way to create a thread in Java is by extending the built-in Thread class. This establishes an "Is-A" relationship, meaning your custom class becomes a specialized type of thread.
Step-by-Step Implementation
- Create a class that extends
Thread. - Override the
run()method. This method contains the code that the thread will execute. - Instantiate your custom class.
- Call the
start()method to begin execution.
// Step 1: Extend the Thread class
class MyThread extends Thread {
// Step 2: Override the run() method to define the task
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
// Step 3: Instantiate the custom thread
MyThread t1 = new MyThread();
// Step 4: Start the thread
t1.start();
}
}
3. Behind-the-Scenes Mechanics of start()
When you call t1.start(), execution does not immediately jump to the run() method. Instead, a complex sequence of operations occurs between the JVM and the host Operating System (OS).
The Execution Flow
Note: Threads are native operating system resources. The JVM does not create threads directly; it delegates thread creation and scheduling to the underlying OS kernel.
4. Thread Creation Method 2: Implementing the Runnable Interface
The second (and highly preferred) way to create a thread is by implementing the Runnable interface. This represents a "Has-A" relationship, where you define a task that can be executed by a thread, rather than defining a thread itself.
Step-by-Step Implementation
- Create a class that implements the
Runnableinterface. - Override the
run()method. - Instantiate your
Runnableclass. - Create a standard
Threadobject and pass yourRunnableinstance to its constructor. - Call
start()on theThreadobject.
// Step 1: Implement the Runnable interface
class MyRunnable implements Runnable {
// Step 2: Override the run() method
@Override
public void run() {
System.out.println("Runnable task is executing on: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
// Step 3: Instantiate the task
MyRunnable task = new MyRunnable();
// Step 4: Pass the task to a Thread constructor
Thread t1 = new Thread(task);
// Step 5: Start the thread
t1.start();
}
}
5. Thread Class vs. Runnable Interface
While both approaches achieve concurrency, they differ significantly in design, flexibility, and production suitability.
The Class Hierarchy
When you implement Runnable directly (e.g., MyRunnable), you bypass the overhead of the Thread class hierarchy and define only the task:
6. Modern Thread Creation: Functional Interfaces & Lambda Expressions
Because Runnable is a Functional Interface (an interface containing exactly one abstract method), we can eliminate boilerplate class definitions entirely by using Java 8+ Lambda expressions.
Code Evolution: From Class to Lambda
Naive Approach (Creating a separate class):
class Task implements Runnable {
public void run() {
System.out.println("Running...");
}
}
Thread t = new Thread(new Task());
Modern Decoupled Approach (Using Lambda Expressions):
public class LambdaThread {
public static void main(String[] args) {
// Define the task and start the thread inline
Thread t1 = new Thread(() -> {
System.out.println("Lambda thread running: " + Thread.currentThread().getName());
});
t1.start();
}
}
7. Thread Identity: Names, IDs, and Deprecation Pitfalls
Every thread in Java has a unique name and an identifier (ID) to help with logging and debugging.
Retrieving Thread Metadata
Thread.currentThread(): Returns a reference to the currently executing thread object.getName(): Returns the thread's name (e.g.,main,Thread-0,Thread-1).getId(): Returns a unique identifier (long) assigned to the thread.
public class ThreadIdentity {
public static void main(String[] args) {
// Accessing the main thread
Thread mainThread = Thread.currentThread();
System.out.println("Main Thread Name: " + mainThread.getName()); // Prints "main"
System.out.println("Main Thread ID: " + mainThread.getId()); // Prints "1" (or other JVM assigned ID)
// Accessing a custom thread
Thread t1 = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println("Custom Thread Name: " + current.getName());
System.out.println("Custom Thread ID: " + current.getId());
});
t1.start();
}
}
The getId() Deprecation Warning
If you compile or run code using getId(), modern Java compilers will flag it with a deprecation warning.
Note: In Java 19,
getId()was deprecated in favor ofthreadId().Why? The original
getId()returned a primitivelongvalue that was arbitrary and did not guarantee any deterministic ordering or execution safety. To align with modern standards and prevent developers from assuming a thread's ID correlates to its execution order, Java introducedthreadId()as a cleaner, standardized replacement.
8. The Critical Difference: start() vs. run()
One of the most common mistakes made by beginners is calling the run() method directly instead of start().
Code Demonstration
public class StartVsRun {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("Executed by: " + Thread.currentThread().getName());
});
// Scenario A: Calling run() directly
System.out.println("--- Calling run() directly ---");
t1.run(); // Output: Executed by: main
// Scenario B: Calling start()
System.out.println("--- Calling start() ---");
t1.start(); // Output: Executed by: Thread-0
}
}
Interview Question: Can we start the same thread twice?
No. Once a thread has finished executing, its lifecycle is complete. Calling start() on an already started or terminated thread will throw an IllegalThreadStateException.
Thread t1 = new Thread(() -> System.out.println("Running"));
t1.start();
t1.start(); // Throws java.lang.IllegalThreadStateException
9. The Non-Deterministic Nature of Thread Execution
Threads are scheduled by the Operating System's CPU Scheduler. Because of this, the exact order in which threads execute, pause, and resume is non-deterministic (unpredictable).
The Even/Odd Interleaving Experiment
Consider two threads: T1 prints even numbers, and T2 prints odd numbers.
public class NonDeterministicExecution {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 2; i <= 20; i += 2) {
System.out.println("T1 (Even): " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 1; i <= 19; i += 2) {
System.out.println("T2 (Odd): " + i);
}
});
t1.start();
t2.start();
}
}
Expected Output (Naive Assumption):
All even numbers print first, followed by all odd numbers (or vice versa).
Actual Output (Reality):
The output is interleaved unpredictably:
T1 (Even): 2
T1 (Even): 4
T2 (Odd): 1
T2 (Odd): 3
T1 (Even): 6
T2 (Odd): 5
...
Running this program multiple times will yield different interleaving patterns.
Factors Influencing Thread Scheduling
- Scheduling Algorithms: The OS uses algorithms like Round Robin, Time Slicing, or Priority-Based Scheduling.
- System Load: The number of other active processes (e.g., Chrome, IDE, OS background tasks) competing for CPU cycles.
- Hardware Architecture: The number of physical CPU cores and hyper-threads available.
- Thread Creation Latency: The slight delay in the OS allocating resources for a newly spawned thread.
10. The 6 States of the Java Thread Lifecycle
A Java thread transitions through distinct states during its existence. These states are defined in the Thread.State enum.
Detailed State Breakdown
Lock Release Behavior
BLOCKED&WAITING: The thread releases any monitor locks it currently holds, allowing other threads to enter the critical section.TIMED_WAITING(viaThread.sleep()): The thread does not release its monitor locks. It holds onto them while sleeping, which can lead to deadlocks if not managed carefully.
The Deprecated stop() Method
The Thread.stop() method instantly forces a thread to transition to the TERMINATED state from any other state. This method is highly deprecated because it forces the thread to release all acquired locks immediately, potentially leaving shared resources in an inconsistent, corrupted state.
11. Step-by-Step Code Walkthrough of State Transitions
The following program demonstrates how to programmatically observe a thread transitioning through the NEW, RUNNABLE, TIMED_WAITING, and TERMINATED states.
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
// Capture a reference to the main thread
Thread mainThread = Thread.currentThread();
// Create a child thread
Thread t1 = new Thread(() -> {
try {
// Print the state of the main thread while it is sleeping
System.out.println("State of Main Thread (inside t1): " + mainThread.getState());
// Keep t1 running for a moment
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 1. Observe NEW State
System.out.println("State of t1 after creation: " + t1.getState()); // Output: NEW
// 2. Start the thread and observe RUNNABLE State
t1.start();
System.out.println("State of t1 after start(): " + t1.getState()); // Output: RUNNABLE
// 3. Put the main thread to sleep to allow t1 to execute
// This puts the main thread into the TIMED_WAITING state
Thread.sleep(2000);
// 4. Observe TERMINATED State
System.out.println("State of t1 after execution completes: " + t1.getState()); // Output: TERMINATED
}
}
Execution Output Analysis
State of t1 after creation: NEW: The thread object exists in memory, but has not been started.State of t1 after start(): RUNNABLE: The thread is registered with the OS and is ready to run.State of Main Thread (inside t1): TIMED_WAITING: Whilet1is executing, it checks the state of themainthread, which is currently paused insideThread.sleep(2000).State of t1 after execution completes: TERMINATED: After themainthread wakes up (after 2 seconds),t1has long completed its 500ms sleep and finished itsrun()method, transitioning toTERMINATED.
12. Summary and Key Takeaways
- Two Creation Paths: You can create threads by extending the
Threadclass (Is-A) or implementing theRunnableinterface (Has-A). ImplementingRunnableis the industry standard because it preserves class inheritance and decouples tasks from execution. - Separation of Concerns: Implementing
Runnableallows you to define what to run, while theThreadclass handles how to run it. - Lambdas for Clean Code: Since
Runnableis a functional interface, you can use Lambda expressions to eliminate boilerplate code when spawning threads. start()vs.run(): Always callstart()to spawn a new OS-level thread. Callingrun()directly executes the code synchronously on the current calling thread.- Non-Deterministic Execution: Thread execution order is controlled by the OS CPU scheduler and is highly unpredictable. Never write code that relies on a specific thread execution order.
- The 6 Lifecycle States: A thread transitions through
NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING, andTERMINATED. - Deprecation Warnings: Methods like
getId()(replaced bythreadId()) andstop()are deprecated in modern Java due to safety and design flaws. - Lock Retention: A sleeping thread (
TIMED_WAITINGviaThread.sleep()) retains its monitor locks, whereas a waiting thread (WAITINGviawait()) releases them.