Question
Java Threads: implements Runnable vs extends Thread
Question
In Java, I have seen two common ways to create work that runs on a separate thread.
One approach is to implement Runnable:
public class MyRunnable implements Runnable {
@Override
public void run() {
// Code
}
}
This is started with:
new Thread(new MyRunnable()).start();
Another approach is to extend Thread:
public class MyThread extends Thread {
public MyThread() {
super("MyThread");
}
@Override
public void run() {
// Code
}
}
This is started with:
new MyThread().start();
Is there any significant difference between these two approaches, and when should one be preferred over the other?
Short Answer
By the end of this page, you will understand the difference between implements Runnable and extends Thread in Java, why they are not exactly the same design choice, and why Runnable is usually preferred in real applications. You will also see how this relates to separation of concerns, code reuse, thread management, and modern Java concurrency practices.
Concept
In Java, both implements Runnable and extends Thread can be used to execute code on a new thread. In both cases, the actual work is placed inside a run() method, and the code begins running on a new thread when start() is called.
However, the two approaches represent different design ideas.
Runnable means “this class defines a task”
A class that implements Runnable describes work to be done.
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Task is running");
}
}
This class is not itself a thread. It is just a task. You pass that task to a Thread object:
Thread thread = new Thread(new MyRunnable());
thread.start();
This separates:
- the job (
Runnable) - the worker thread (
Thread)
That separation is usually better design.
Thread means “this class is itself a thread”
When you extend Thread, your class becomes a specialized thread type.
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
Then you create and start it directly:
MyThread thread = new MyThread();
thread.start();
This works, but it mixes the task logic and the thread object into one class.
Why Runnable is usually preferred
1. Better separation of concerns
With Runnable, your class focuses only on the work. The thread is just a mechanism for running that work.
2. Java allows only single inheritance
If your class extends Thread, it cannot extend any other class.
class MyTask extends SomeOtherBaseClass implements Runnable {
@Override
public void run() {
// allowed
}
}
But this is not possible if you already extend Thread.
3. Easier reuse with thread pools and executors
Modern Java code often uses ExecutorService instead of manually creating threads. ExecutorService accepts Runnable tasks directly.
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new MyRunnable());
executor.shutdown();
A Runnable fits naturally here.
4. Cleaner object modeling
A business task is usually not conceptually a thread.
For example:
- sending an email
- processing an order
- downloading a file
- cleaning expired sessions
These are tasks, not thread types.
Important shared behavior
No matter which approach you use:
run()contains the code to executestart()creates a new thread and then callsrun()internally- calling
run()directly does not start a new thread
When extending Thread might be acceptable
Extending Thread is not wrong. It can be acceptable when:
- you truly want to customize thread behavior
- you need a named thread subclass with extra thread-related logic
- you are learning how threads work at a basic level
But in most application code, Runnable is the better choice.
Mental Model
Think of Runnable as a job description and Thread as a worker.
- A
Runnablesays: “Here is the work that needs to be done.” - A
Threadsays: “I am the worker that will perform work.”
Using implements Runnable is like writing a task on a piece of paper and handing it to any available worker.
Using extends Thread is like creating a brand-new worker and permanently attaching the task to that worker.
In real systems, it is usually more flexible to keep the task separate from the worker. That way, many different workers can execute the same kind of task, and task objects can also be reused with thread pools.
Syntax and Examples
Core syntax
Using Runnable
class PrintTask implements Runnable {
@Override
public void run() {
System.out.println("Running task in: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new PrintTask());
thread.start();
}
}
Using Thread
class PrintThread extends Thread {
@Override
public void run() {
System.out.println("Running thread: " + getName());
}
}
public class Main {
public static void main(String[] args) {
PrintThread thread = new PrintThread();
thread.start();
}
}
A more realistic Runnable example
class FileUploadTask implements Runnable {
private final String fileName;
public FileUploadTask(String fileName) {
this.fileName = fileName;
}
@Override
public void run() {
System.out.println("Uploading " + fileName + " on " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(new FileUploadTask("report.pdf"));
Thread t2 = new Thread(new FileUploadTask("image.png"));
t1.start();
t2.start();
}
}
This is beginner-friendly and practical because:
- the task has its own data (
fileName) - different
Threadobjects can run different task instances - the task logic stays separate from thread creation
Modern style with lambda
Because Runnable is a functional interface, Java also allows this:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
}
}
This is common for short tasks.
Step by Step Execution
Consider this example:
class HelloTask implements Runnable {
@Override
public void run() {
System.out.println("Hello from " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Main starts");
Thread thread = new Thread(new HelloTask());
thread.start();
System.out.println("Main ends");
}
}
What happens step by step
1. main() begins running
The JVM starts the main thread and executes:
System.out.println("Main starts");
Output so far:
Main starts
2. A HelloTask object is created
new HelloTask()
This object only holds the task logic. It is not yet running.
3. A Thread object is created
Thread thread = new Thread(new HelloTask());
Now Java has a new thread object that knows which task to execute.
4. start() is called
thread.start();
This is the important part:
- a new thread is created by the JVM
- that new thread eventually calls the task's
run()method
5. The main thread continues immediately
System.out.println("Main ends");
The main thread does not wait unless you explicitly tell it to.
6. The new thread runs run()
System.out.println("Hello from " + Thread.currentThread().getName());
Possible output
Main starts
Main ends
Hello from Thread-0
Or sometimes:
Main starts
Hello from Thread-0
Main ends
Both are valid, because thread scheduling is not guaranteed.
Key lesson
Calling start() creates concurrent execution. The exact print order may vary.
If you called run() directly instead:
thread.run();
then no new thread would be created. It would just run like a normal method call on the current thread.
Real World Use Cases
Where this concept is used
Background processing
Applications often need to do work without blocking the main flow:
- sending emails
- generating reports
- resizing images
- syncing data
These are naturally modeled as Runnable tasks.
Server-side request handling
In web servers and backend systems, units of work are often executed by managed worker threads. Even when you do not create Thread objects manually, the underlying idea is still task execution on threads.
Scheduled jobs
Periodic work such as:
- clearing cache
- polling an API
- refreshing tokens
- archiving logs
can be represented as Runnable tasks and run by schedulers.
Desktop applications
In GUI applications, long-running work should not freeze the interface. A background task can be defined separately and executed on another thread.
Data processing pipelines
If multiple independent items need processing, each processing step can be represented as a task and submitted to executors.
Real Codebase Usage
In real Java codebases, developers usually avoid manually subclassing Thread unless they have a strong reason.
Common patterns
1. Task classes implement Runnable
class CleanupTask implements Runnable {
@Override
public void run() {
// cleanup logic
}
}
This makes the task easy to test and reuse.
2. Use ExecutorService instead of new Thread(...)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(new CleanupTask());
executor.shutdown();
This is more scalable than creating raw threads everywhere.
3. Guard clauses inside tasks
Developers often validate state early:
class EmailTask implements Runnable {
private final String email;
public EmailTask(String email) {
this.email = email;
}
@Override
public void run() {
if (email == null || email.isBlank()) {
return;
}
System.out.println("Sending email to " + email);
}
}
4. Error handling inside background work
Exceptions in worker threads should be handled carefully:
class SafeTask implements Runnable {
@Override
public void run() {
try {
System.out.println("Doing background work");
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. Configuration and dependency injection
A task often receives dependencies through its constructor:
class ReportTask implements Runnable {
private final ReportService reportService;
public ReportTask(ReportService reportService) {
this.reportService = reportService;
}
@Override
public void run() {
reportService.generate();
}
}
This is much cleaner than embedding everything inside a custom Thread subclass.
Common Mistakes
1. Calling run() instead of start()
Broken code:
Thread thread = new Thread(new MyRunnable());
thread.run();
Why it is wrong:
run()is just a normal method call- no new thread is created
Correct code:
thread.start();
2. Thinking Runnable creates a thread by itself
Broken assumption:
MyRunnable task = new MyRunnable();
// task is not running yet
A Runnable only defines work. It must be passed to a Thread or executor.
3. Extending Thread when inheritance is needed elsewhere
Broken design:
class MyWorker extends Thread {
// now this class cannot extend another base class
}
If your class should also inherit from another class, extending Thread becomes limiting.
4. Mixing task logic with thread lifecycle logic
When a class both defines work and represents the actual thread, the design can become harder to test and reuse.
Prefer:
class Task implements Runnable {
@Override
public void run() {
// work only
}
}
5. Ignoring exceptions in background code
Exceptions inside threads may not behave the way beginners expect. Always consider logging or handling them.
class RiskyTask implements Runnable {
@Override
public void run() {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Handled: " + e.getMessage());
}
}
}
6. Assuming execution order is fixed
Broken expectation:
thread.start();
System.out.println("This always prints second");
Thread scheduling is unpredictable. Do not rely on output order unless you synchronize explicitly.
Comparisons
implements Runnable vs extends Thread
| Aspect | implements Runnable | extends Thread |
|---|---|---|
| What it represents | A task | A thread object |
| Inheritance flexibility | Can still extend another class | Cannot extend another class |
| Separation of concerns | Better | Weaker |
| Reuse with executors | Natural fit | Less natural |
| Beginner simplicity | Slightly more setup | Slightly shorter syntax |
| Recommended in real apps | Usually yes | Usually no |
run() vs start()
| Method | What it does |
|---|---|
run() | Executes code like a normal method call on the current thread |
start() | Starts a new thread, which then calls run() |
Runnable vs Callable
| Feature | Runnable | Callable<T> |
|---|---|---|
| Returns a value | No | Yes |
| Can throw checked exceptions | No | Yes |
| Common use | Simple background tasks | Tasks that produce results |
Callable is useful later, but for this question, Runnable is the core concept.
Cheat Sheet
Quick rules
- Use
implements Runnableto define a task. - Use
new Thread(task).start()to run that task on a new thread. extends Threadalso works, but is usually less flexible.- Prefer
Runnablein most real Java code. - Never call
run()when you mean to start a new thread. - Use
ExecutorServicefor production-style concurrency.
Basic syntax
class MyTask implements Runnable {
@Override
public void run() {
// work here
}
}
Thread t = new Thread(new MyTask());
t.start();
Alternative syntax
class MyThread extends Thread {
@Override
public void run() {
// work here
}
}
new MyThread().start();
Best practice
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new MyTask());
executor.shutdown();
Remember
Runnable= workThread= workerstart()= begin concurrent executionrun()= normal method body
Edge case
If you need a result from background work, look into Callable and Future.
FAQ
Is implements Runnable better than extends Thread in Java?
Usually, yes. Runnable is more flexible, separates the task from the thread, and works better with executors and thread pools.
Do both approaches create a new thread?
Yes, if you call start(). In both cases, a new thread is created and run() executes on that thread.
What happens if I call run() directly?
It runs on the current thread like a normal method call. No new thread is created.
Why is extending Thread considered less flexible?
Because Java supports only single inheritance. If your class extends Thread, it cannot extend any other class.
Can I use lambda expressions with Runnable?
Yes. Runnable is a functional interface, so lambdas work well for short tasks.
Is extends Thread ever useful?
Yes, but mostly in special cases where you need a custom thread type. For most application logic, Runnable is preferred.
What is the modern Java way to run background tasks?
Usually ExecutorService, often with Runnable or Callable, instead of manually creating many Thread objects.
Mini Project
Description
Build a small Java program that runs multiple background tasks to simulate processing files. This project demonstrates why Runnable is useful: each task describes a unit of work, while separate Thread objects execute that work. It mirrors real applications where jobs such as uploads, report generation, or cleanup run independently.
Goal
Create a program that starts several file-processing tasks concurrently using Runnable and prints which thread handles each task.
Requirements
- Create a class that implements
Runnable. - Pass task-specific data such as a file name into the task.
- Start at least three separate
Threadobjects using different task instances. - Print the current thread name and the file being processed.
- Wait for all threads to finish before the program exits.
Keep learning
Related questions
Add External JAR Files to an IntelliJ IDEA Java Project
Learn how to add external JAR dependencies to an IntelliJ IDEA Java project using module libraries, and when to use Maven or Gradle instead.
Avoiding Java Code in JSP with JSP 2: EL and JSTL Explained
Learn how to avoid Java scriptlets in JSP 2 using Expression Language and JSTL, with examples, best practices, and common mistakes.
Call a Method After a Delay in Android Java
Learn how to run Java code after a delay in Android using Handler.postDelayed, manage the main thread, and cancel callbacks safely.