public class Main {
public static void main(String[] args) {
System.out.println("Main thread is running");
}
}Creating Threads in Java: Extending Thread vs Implementing Runnable
Direct Answer & Core Concept
Creating a thread in Java means defining a task that can run independently from the main flow of the program. Java provides several concurrency APIs, but two traditional approaches are extending the Thread class and implementing the Runnable interface.
Understanding these two approaches is important because they teach the basic relationship between a thread and the task performed by that thread. In modern Java applications, Runnable and higher-level concurrency tools are often preferred when the task should be separated from thread management.
Detailed Explanation & Mechanics
What Is a Thread in Java?
A thread is a lightweight unit of execution inside a Java program. A Java application can have multiple threads running different tasks concurrently.
For example, one thread might process user input while another performs background work.
Every Java application starts with a main thread that executes the main() method. Additional threads can be created when the program needs concurrent execution.
Two Traditional Ways to Create a Thread
Java provides two commonly taught approaches:
Extend the
ThreadclassImplement the
Runnableinterface
Both can create a thread that executes code inside its run() method.
Basic idea
Thread | +---- run() | +---- Task codeThe important distinction is that Thread represents the execution mechanism, while Runnable represents a task that can be executed by a thread.
1. Creating a Thread by Extending Thread
The first approach is to create a class that extends Java's Thread class.
You then override the run() method and place the thread's work inside it.
Example
class MyThread extends Thread {
@Override public void run() {
System.out.println("Thread is running");
}
}public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}Here, MyThread inherits from Thread. The run() method contains the code that should execute when the thread starts.
The start() method is important because it starts a new thread and eventually causes run() to execute on that new thread.
Understanding start() vs run()
One of the most important concepts when creating Java threads is the difference between calling start() and calling run() directly.
Correct way
MyThread thread = new MyThread();
thread.start();Calling start() asks the JVM to start a new thread.
Not the same
thread.run();Calling run() directly is simply a normal method call. It does not create a new thread.
Example
class MyThread extends Thread {
@Override public void run() {
System.out.println("Running in: " + Thread.currentThread().getName());
}
}public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.run();
}
}In this case, run() executes as part of the current thread rather than starting a new thread.
So remember:
start() → starts a new threadrun() → executes the method normally when called directly2. Creating a Thread by Implementing Runnable
The second approach is to implement the Runnable interface.
Runnable represents a task that can be executed by a thread.
Example
class MyTask implements Runnable {
@Override public void run() {
System.out.println("Task is running");
}
}public class Main {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task);
thread.start();
}
}Here, MyTask does not become a thread itself. It defines the work that a thread should perform.
The Thread object is responsible for running that task.
How Runnable Works
The relationship can be visualized like this:
Runnable | | contains v Task | | passed to v Thread | | start() vExecutionThis separation is one of the main advantages of the Runnable approach.
The task describes what should be done, while the thread describes where/how the task is executed.
Thread vs Runnable Example
Here are the two approaches side by side.
Extending Thread
class MyThread extends Thread {
@Override public void run() {
System.out.println("Running task");
}
}MyThread thread = new MyThread();
thread.start();Implementing Runnable
class MyTask implements Runnable {
@Override public void run() {
System.out.println("Running task");
}
}Thread thread = new Thread(new MyTask());
thread.start();Both approaches can execute the task on a separate thread.
Passing a Runnable to Thread
The Thread class provides constructors that accept a Runnable.
For example:
Runnable task = new Runnable() {
@Override public void run() {
System.out.println("Hello from the task");
}
};
Thread thread = new Thread(task);
thread.start();You can also write this more compactly using a lambda expression.
Runnable task =() -> {
System.out.println("Hello from the task");
};
Thread thread = new Thread(task);
thread.start();This style is common in modern Java code.
Runnable With Lambda Expressions
Because Runnable is a functional interface, it can be represented using a lambda expression.
Instead of:
Runnable task = new Runnable() {
@Override public void run() {
System.out.println("Processing data...");
}
};You can write:
Runnable task =() -> {
System.out.println("Processing data...");
};Or even:
Thread thread = new Thread(() -> System.out.println("Processing data..."));
thread.start();This is useful for small tasks where creating a separate class would add unnecessary code.
Extending Thread: Advantages
Extending Thread is simple and easy to understand for beginners.
Simple syntax
The thread and its task are combined into one class.
class Worker extends Thread {
@Override public void run() {
System.out.println("Worker running");
}
}Direct access to Thread methods
Because the class itself extends Thread, it can directly use methods such as:
start();
sleep(1000);
getName();
interrupt();However, this convenience does not necessarily make it the best design for larger applications.
Extending Thread: Limitation
Java does not support multiple class inheritance.
For example:
class MyThread extends Thread {
}Because MyThread already extends Thread, it cannot extend another class.
This can become a problem if the class needs to inherit from another superclass.
For example:
class Employee {
// Employee functionality
}You cannot write:
class Worker extends Employee, Thread {
}Java does not allow this form of multiple inheritance.
This is one reason implementing Runnable can provide more flexibility.
Runnable: Advantages
Implementing Runnable separates the task from the thread.
class FileProcessor implements Runnable {
@Override public void run() {
System.out.println("Processing file");
}
}The class can still extend another class if necessary.
class Employee {
// Employee functionality
}class FileProcessor extends Employee implements Runnable {
@Override public void run() {
System.out.println("Processing file");
}
}This works because Java allows a class to extend one class and implement interfaces.
Runnable Encourages Task Separation
One of the biggest conceptual advantages of Runnable is separating task logic from thread management.
For example:
class EmailTask implements Runnable {
@Override public void run() {
System.out.println("Sending email...");
}
}The task does not need to know how the thread is managed.
Another part of the application can decide how to execute it:
Runnable task = new EmailTask();
Thread thread = new Thread(task);
thread.start();The same task can also be used with higher-level concurrency APIs.
Reusing the Same Runnable Task
A Runnable object represents a task, so it can potentially be supplied to different Thread objects.
Runnable task =() -> {
System.out.println( "Running in " + Thread.currentThread().getName());
};
Thread thread1 = new Thread(task, "Worker-1");
Thread thread2 = new Thread(task, "Worker-2");
thread1.start();
thread2.start();Each thread executes the same task logic.
The output may look like:
Running in Worker-1Running in Worker-2The exact order of thread output is not guaranteed.
Example: Multiple Threads
Let's create several worker threads.
class Worker implements Runnable {
private final String workerName;
public Worker(String workerName) {
this.workerName = workerName;
} @Override public void run() {
for (int i = 1;
i <= 3;
i++) {
System.out.println( workerName + " - step " + i);
}
}
}public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new Worker("Worker A"));
Thread t2 = new Thread(new Worker("Worker B"));
t1.start();
t2.start();
}
}Because both threads execute concurrently, their output may be interleaved.
For example:
Worker A - step 1Worker B - step 1Worker A - step 2Worker B - step 2Worker B - step 3Worker A - step 3Another execution may produce a different order.
Do not write multithreaded code assuming a particular scheduling order unless synchronization or another coordination mechanism guarantees it.
Thread Names
Giving threads meaningful names makes debugging easier.
Runnable task =() -> {
System.out.println( Thread.currentThread().getName());
};
Thread thread = new Thread(task, "File-Worker");
thread.start();You can retrieve the current thread with:
Thread.currentThread()And retrieve its name with:
Thread.currentThread().getName()Meaningful thread names are particularly useful when reading logs from applications with many concurrent operations.
Can a Thread Be Started Twice?
A Thread object can only be started once.
This is invalid:
Thread thread = new Thread(() -> System.out.println("Hello"));
thread.start();
thread.start();Attempting to start the same Thread instance again results in IllegalThreadStateException.
If you need another thread, create another Thread object.
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();The same Runnable task can be used with separate thread objects.
Constructor With Runnable
You can create a thread and provide its task directly.
Runnable task =() -> {
System.out.println("Task executing");
};
Thread thread = new Thread(task);
thread.start();You can also give the thread a name:
Thread thread = new Thread( task, "Background-Worker");
thread.start();This makes the relationship between task and execution mechanism clear.
Extending Thread vs Implementing Runnable
The two approaches can be compared conceptually:
| Feature | Extending Thread | Implementing Runnable |
|---|---|---|
| Defines task | run() in subclass | run() in implementation |
| Creates thread | Class itself is a Thread | Thread object executes task |
| Can extend another class | No | Yes |
| Separates task from thread | Less | Better |
| Can use lambda directly | No | Yes |
| Task reuse | Less flexible | More flexible |
| Beginner simplicity | Very simple | Simple |
| Common design choice | Useful for learning/specialized cases | Often preferred for task-based design |
The key difference is not that one magically creates “more concurrency” than the other. Both ultimately use a Thread to perform the work.
Which Approach Should You Learn First?
For understanding Java's thread fundamentals, it is useful to learn both.
Start with Thread because it makes the basic lifecycle easy to visualize:
create Thread↓start()↓run()↓thread completesThen learn Runnable because it introduces the important separation between a task and the thread that executes it.
For application design, Runnable is often a better abstraction when you have a task that should be independent from the mechanism used to execute it.
Runnable and ExecutorService
In production applications, you will often move beyond manually creating threads.
For example:
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> {
System.out.println("Task running");
});
executor.shutdown();Here, the application submits a task to an executor rather than manually creating a new Thread for every task.
This approach provides higher-level management of threads and task execution.
For many server-side applications, executors are more practical than creating large numbers of raw threads manually.
Thread vs Runnable vs ExecutorService
Think of these concepts at different levels:
Thread↓Low-level execution mechanismRunnable↓A task that can be executedExecutorService↓Higher-level task/thread managementA useful mental model is:
WHAT should happen?↓RunnableHOW should it execute?↓Thread / ExecutorServiceThis separation becomes increasingly important as applications become larger.
Common Mistakes
Mistake 1: Calling run() Instead of start()
thread.run();This does not start a new thread.
Use:
thread.start();Mistake 2: Starting the Same Thread Twice
thread.start();
thread.start();A Thread instance cannot be restarted.
Create a new thread instead.
Mistake 3: Assuming Thread Order
thread1.start();
thread2.start();This does not guarantee that thread1 will finish before thread2.
Thread scheduling is controlled by the JVM and operating system.
Mistake 4: Creating Too Many Raw Threads
Creating a huge number of threads manually can create unnecessary resource and scheduling overhead.
For larger applications, consider higher-level concurrency tools such as ExecutorService or virtual threads where appropriate.
Modern Java: Virtual Threads
Modern Java also provides virtual threads, which are lightweight threads designed to make high-concurrency applications easier to build.
For example:
Thread.startVirtualThread(() -> {
System.out.println("Running in a virtual thread");
});Virtual threads do not replace the need to understand Thread and Runnable. Instead, they provide another execution model for suitable workloads.
They are especially useful when applications have many concurrent tasks that spend significant time waiting on blocking operations.
Practical Example: Download Task
Suppose an application needs to process a download task.
Using Runnable
class DownloadTask implements Runnable {
private final String fileName;
public DownloadTask(String fileName) {
this.fileName = fileName;
} @Override public void run() {
System.out.println( "Downloading " + fileName);
}
}public class Main {
public static void main(String[] args) {
Runnable task = new DownloadTask("report.pdf");
Thread thread = new Thread(task, "Download-Worker");
thread.start();
}
}The DownloadTask describes the work, while Thread provides the execution mechanism.
This separation makes the design easier to adapt later if the application moves to an executor or another concurrency mechanism.
When to Use Each Approach
Extending Thread
Use it when:
You specifically need a specialized
Threadsubclass.You are learning the fundamentals of Java threads.
Combining the task and thread behavior is intentional.
Implementing Runnable
Use it when:
You want to separate task logic from thread management.
Your class already needs to extend another class.
You want to use a lambda expression.
You expect the task to be submitted to an executor.
You want the task abstraction to remain reusable.
ExecutorService
Consider it when:
The application has many tasks.
You need controlled thread management.
You want thread pools.
You want to submit tasks without manually creating a new thread for every operation.
Simple Mental Model
Remember this distinction:
extends Thread↓"My class IS a thread."implements Runnable↓"My class IS a task."Thread(task)↓"This thread executes the task."This simple model helps explain why Runnable is commonly preferred when designing task-oriented concurrent code.
Frequently Asked Questions
Is Runnable a thread?
No. Runnable represents a task. A Thread can execute that task.
Does calling run() create a thread?
No. Calling run() directly is a normal method call. Calling start() begins execution on a new thread.
Can a class extend Thread and another class?
No. Java allows a class to extend only one class.
Can a class extend another class and implement Runnable?
Yes.
class Worker extends Employee implements Runnable {
@Override public void run() {
System.out.println("Working...");
}
}Can the same Runnable be used by multiple threads?
Yes.
Runnable task =() -> System.out.println("Working");
new Thread(task).start();
new Thread(task).start();The task's behavior may execute concurrently on separate threads. If the task accesses shared mutable state, appropriate synchronization or other concurrency controls may be necessary.
Can a Thread be started twice?
No. A Thread instance can only be started once.
Which is better, Thread or Runnable?
They solve slightly different design problems. Thread combines thread behavior with the class, while Runnable represents a task separately. For task-oriented design, Runnable provides greater separation and flexibility.
Is Runnable still useful with modern Java?
Yes. Runnable remains a fundamental task abstraction and is used with threads, executors, and other concurrency APIs.
Runnable Code Example: Creating Threads in Java
Java 17+import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadCreationDemo {
public static void main(String[] args) throws Exception {
//
1. Runnable with Lambda Runnable task =() -> System.out.println("Runnable running on " + Thread.currentThread().getName());
new Thread(task).start();
//
2. Callable with Return Value Callable<Integer> callableTask =() -> 42 * 2;
FutureTask<Integer> future = new FutureTask<>(callableTask);
new Thread(future).start();
System.out.println("Callable Result: " + future.get());
}
}