Question

Java Threads: implements Runnable vs extends Thread

javamultithreadingrunnableimplementsjava-threads

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 execute
  • start() creates a new thread and then calls run() 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 Runnable says: “Here is the work that needs to be done.”
  • A Thread says: “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.

Take Quiz

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 Thread objects 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.

Take Quiz

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

Aspectimplements Runnableextends Thread
What it representsA taskA thread object
Inheritance flexibilityCan still extend another classCannot extend another class
Separation of concernsBetterWeaker
Reuse with executorsNatural fitLess natural
Beginner simplicitySlightly more setupSlightly shorter syntax
Recommended in real appsUsually yesUsually no

run() vs start()

MethodWhat 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

FeatureRunnableCallable<T>
Returns a valueNoYes
Can throw checked exceptionsNoYes
Common useSimple background tasksTasks that produce results

Callable is useful later, but for this question, Runnable is the core concept.

Take Quiz

Cheat Sheet

Quick rules

  • Use implements Runnable to define a task.
  • Use new Thread(task).start() to run that task on a new thread.
  • extends Thread also works, but is usually less flexible.
  • Prefer Runnable in most real Java code.
  • Never call run() when you mean to start a new thread.
  • Use ExecutorService for 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 = work
  • Thread = worker
  • start() = begin concurrent execution
  • run() = 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.

Take Quiz

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 Thread objects using different task instances.
  • Print the current thread name and the file being processed.
  • Wait for all threads to finish before the program exits.
Take Quiz

Keep learning

Related questions

View more