Sitelet https://howtodoinjava.com/java/multi-threading/java-runnable-vs-thread/#comment-677

Runnable vs Thread in Java: Differences and When to Use Each

Runnable vs Thread in Java compared with examples, a difference table, shared tasks, executors and virtual threads, and when extending Thread still fits.

Class diagram showing Thread implementing Runnable, a Thread subclass and a Runnable task passed to a Thread

In Java, Runnable is an interface that describes a task to run, whereas Thread is the class that runs a task on its own thread of execution. In the Runnable vs Thread choice, we implement Runnable (or write a lambda) and pass it to a Thread or an executor, and we extend Thread only when we change how the thread itself behaves.

The question comes up every time we write background work, such as sending a receipt email after checkout or refreshing a cache, and the answer decides whether that code can later move to a thread pool or a virtual thread without a rewrite.

The following example runs the same kind of work both ways and counts the sent receipts.

AtomicInteger sent = new AtomicInteger();
Runnable sendReceipt = sent::incrementAndGet;     // a task, no thread yet

Thread worker = new Thread(sendReceipt);          // Runnable handed to a Thread
worker.start();
worker.join();

Thread subclass = new Thread() {                  // Thread subclass overriding run()
    @Override
    public void run() {
        sent.incrementAndGet();
    }
};
subclass.start();
subclass.join();
int total = sent.get();                           // 2

Notice that both versions end with start() on a Thread object, because only a Thread can start a new thread. The difference is where the work lives. We look at each type first, compare them in a table, and finish with the rare cases where subclassing Thread still fits.

1. Thread Class

The Thread class represents a thread of execution in a Java program. It holds the thread name, priority, daemon flag and state, and its start() method asks the JVM to create the thread and call run() on it. To use it by inheritance, we extend Thread in a child class and override run().

class ReceiptThread extends Thread {
    private final String orderId;

    ReceiptThread(String orderId) {
        super("receipt-" + orderId);
        this.orderId = orderId;
    }

    @Override
    public void run() {
        ReceiptLog.record(orderId + " sent by " + getName());
    }
}
class ReceiptLog {
    static final List<String> LINES = new CopyOnWriteArrayList<>();

    static void record(String line) {
        LINES.add(line);
    }
}
ReceiptThread thread = new ReceiptThread("A7");
thread.start();
thread.join();
String line = ReceiptLog.LINES.getLast();         // "A7 sent by receipt-A7"

The Thread class itself implements Runnable. Its own run() method calls the Runnable passed to the constructor, or does nothing if there is none, which is why a subclass must override run() to do any work.

2. Runnable Interface

A class implements Runnable when its instances are meant to be executed by a thread. The interface has one method, void run(), with no parameters, no return value and no checked exceptions. Since Java 8, Runnable is annotated as a functional interface, so a lambda or a method reference can implement it.

class SendReceiptTask implements Runnable {
    private final String orderId;

    SendReceiptTask(String orderId) {
        this.orderId = orderId;
    }

    @Override
    public void run() {
        ReceiptLog.record(orderId + " sent by " + Thread.currentThread().getName());
    }
}

To run the task on a new thread, we create a Thread, pass the task to the constructor and call start(). The same object also works with an executor or a virtual thread, because they all accept a Runnable.

Thread platform = new Thread(new SendReceiptTask("B2"), "mailer");
platform.start();
platform.join();
String first = ReceiptLog.LINES.getLast();        // "B2 sent by mailer"

Thread virtual = Thread.ofVirtual().name("vmail").start(new SendReceiptTask("C3"));
virtual.join();
String second = ReceiptLog.LINES.getLast();       // "C3 sent by vmail"

For tasks that must return a value or throw a checked exception, Java offers Callable instead, which is compared in Runnable vs Callable.

3. Difference between Runnable vs. Thread

The core difference is inheritance against composition. A Thread subclass is a thread, so the task and the execution mechanism are one object. A Runnable is only the task, and a separate Thread or executor runs it, so each part can change on its own.

Class diagram showing Thread implementing Runnable, a Thread subclass and a Runnable task passed to a Thread
A Thread subclass is the task and the thread at once; a Runnable is only the task and can go to a Thread, an executor or a virtual thread

In day-to-day code, the differences show up in inheritance, lambda support, reuse of one instance and how well the task fits executors and virtual threads.

FeatureRunnableThread
TypeFunctional interface in java.langClass in java.lang that implements Runnable
InheritanceThe task class can still extend another classThe subclass cannot extend any other class
Lambda supportYes, () -> work()No, a subclass needs a class body
Reuse of one instanceOne instance can run on many threads, one after another or at the same timeOne instance can be started only once
Executors and virtual threadsAccepted by ExecutorService, CompletableFuture.runAsync() and Thread.ofVirtual()Only as a plain Runnable, so its thread features are wasted
Thread control (name, priority, daemon)Set on the Thread or builder that runs the taskSet on the subclass instance itself
MemoryThe task object plus a Thread object to run itOne object that is both, so no real saving

The old claim that Runnable saves memory because no thread object is created is wrong. A task always needs a Thread to run on. The real costs are the operating system thread behind each platform thread and its stack, which are the same in both styles.

3.1. Sharing One Task Between Threads

Because a Runnable is separate from the thread, several threads can run the same instance. Any state in the task is shared by all of them, so it must be thread-safe, for example an AtomicInteger instead of an int field.

class PageViewCounter implements Runnable {
    final AtomicInteger views = new AtomicInteger();

    @Override
    public void run() {
        views.incrementAndGet();
    }
}
PageViewCounter counter = new PageViewCounter();
List<Thread> threads = List.of(new Thread(counter), new Thread(counter), new Thread(counter));
for (Thread t : threads) {
    t.start();
}
for (Thread t : threads) {
    t.join();
}
int views = counter.views.get();                  // 3, one shared task

With a Thread subclass, each started thread is a new instance with its own fields. Sharing state is still possible through a static field or a shared object, but it no longer follows from the design.

3.2. Moving a Task to an Executor or Virtual Thread

Production code runs tasks on an ExecutorService so threads are reused or, with virtual threads, created cheaply per task. A Runnable moves there without any change.

int before = ReceiptLog.LINES.size();
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(new SendReceiptTask("D4"));
    executor.submit(new SendReceiptTask("E5"));
}                                                 // close() waits for both tasks
int added = ReceiptLog.LINES.size() - before;     // 2

A Thread subclass can be submitted too, because it is a Runnable, but the executor calls only its run() method on a pool thread. The subclass object is never started, so its name, priority and daemon settings are ignored, which confuses readers of the code and of the logs.

4. When Extending Thread Still Makes Sense

Extending Thread is reasonable only when the class changes thread behavior rather than describing a task. The JDK does it in ForkJoinWorkerThread, and frameworks subclass it to attach per-thread resources or to clean up in run() after the task completes.

Even in those cases, the subclass is created by a ThreadFactory and runs tasks given to it, so application code still writes Runnable tasks. For one-off threads, the Thread builders added in Java 21 set the name, daemon flag and priority without a subclass.

5. Sending Receipts After Checkout

An online shop saves the order and must send a receipt email, but the customer should not wait for the mail server. The checkout code hands a SendReceiptTask to an executor and returns. Because the task is a plain Runnable, a unit test calls run() on the test thread and checks the result without starting any thread.

SendReceiptTask task = new SendReceiptTask("F6");
task.run();                                       // runs on the calling thread
String inTest = ReceiptLog.LINES.getLast();       // "F6 sent by main"

When the shop later moves from a fixed thread pool to virtual threads, only the line that creates the executor changes. A ReceiptThread subclass would need its callers rewritten, since every call site creates and starts a new thread object. The broader picture of executors, locks and concurrent collections is in the Java concurrency guide.

6. Runnable vs Thread FAQs

Mixing both styles in one codebase raises a few practical doubts, mostly about inheritance, run() and passing one Thread to another.

6.1. Is it better to implement Runnable or extend Thread?

Implement Runnable. It keeps the class free to extend another class, works with lambdas, executors and virtual threads, and can be tested without starting a thread. Extend Thread only when the class customizes the thread itself.

6.2. Why does the Thread class implement Runnable?

So that a Thread has a run() method to call after start(). The default Thread.run() calls the Runnable passed to the constructor, which is how new Thread(task) works, and a subclass replaces it with its own code.

6.3. What happens if we call run() instead of start()?

The task runs on the current thread like an ordinary method call, and no new thread is created. Only start() runs run() concurrently.

6.4. Can a class extend Thread and implement Runnable at the same time?

Yes, it compiles, but it adds nothing, because Thread already implements Runnable. Such a class still has every drawback of a Thread subclass.

6.5. Can we pass a Thread object to another Thread’s constructor?

Yes, because a Thread is a Runnable. The new thread calls the inner object’s run() method, while the inner thread is never started, which makes the code misleading. Pass a Runnable instead.

7. Conclusion

Implementing Runnable is the preferred way. We are not specializing or modifying the thread’s behavior, we are giving the thread something to run, so composition fits better than inheritance.

A Runnable task moves between a plain Thread, a pool, CompletableFuture and virtual threads without a change, and it can be tested on the calling thread. Extending Thread remains for the few classes that change how threads themselves work.

8. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. At point 4, how can a task(non-run metho) inside Thread extending class, still need to run in thread only. It can still be executed within a main thread. Could u pls elaborate?

  2. By implementing Runnable interface, we can only override the run() method. But when we extend Thread class, we can use many methods based on our requirements. If we want to make our class as a thread, we can always go for extending the Thread class rather than implementing Runnable interface.

    • Hello Sowmya rajan,

      As per my understanding, Thread is a curse in java. If you create multiple threads then there would be a chance of memory overhead and hence the performance of the app will go down.

  3. As per my knowledge, the advantage is – when there are multiple threads then, memory usage would be more in case of extends Thread. Because, each of your threads contains unique object associated with it. Where as, memory usage would be less in case of implements Runnable. Because, many threads can share the same runnable instance.

  4. Awesome explanation !!
    Small correction in point 4

    It should be ” implements Runnable” Not “extend Runnable”

  5. 5) By extending Thread, each of your threads has a unique object associated with it, whereas implementing Runnable, many threads can share the same runnable instance. – This statement seems wrong to me.

    Many threads can share the same Thread object, same as Runnable instance.
    For example,
    public class A1 implements Runnable {….}
    public class B1 extends Thread {….}

    public class ABMain {
    public static void main( String[] args ) {
    A1 a = new A1(); //Create runnable instance
    B1 b = new B1(); // Create Thread instance

    new Thread( a ).start(); //Starts runnable instance

    new Thread( b ).start(); //Starts Thread instance.

    new Thread( a ).start(); // 2nd thread sharing same runnable instance A1
    new Thread( b ).start(); // 2nd thread sharing same Thread instance.

    }
    }

    Difference between Thread and Runnable I found is, Thread class provides apis to configure Thread instance based on your need.
    If you extends Thread class, then
    1. you will have flexibility to set priority to Thread instance,
    2. you can create daemon thread,
    3. you can check if thread is terminated, alive etc.

    • There are apis available in Thread class which is not avaialbe in Runnable. Those apis helps developers to configure Thread properties.

    • I modified your program a little bit, to correct the picture of usage of thread and runnable.

      class A1 implements Runnable {
      private int counter = 0;
      @Override
      public void run() {
      counter++;
      System.out.println("Running A1 -> " + counter);
      }
      }

      class B1 extends Thread {
      private int counter = 0;
      @Override
      public void run() {
      counter++;
      System.out.println("Running B1 -> " + counter);
      }
      }

      public class ABMain {
      public static void main(String[] args) throws InterruptedException {
      A1 a = new A1(); // Create runnable instance
      B1 b = new B1(); // Create Thread instance

      new Thread(a).start();
      Thread.sleep(1000);
      new Thread(a).start();
      Thread.sleep(1000);
      new Thread(a).start();
      Thread.sleep(1000);

      new B1().start();
      Thread.sleep(1000);
      new B1().start();
      Thread.sleep(1000);
      new B1().start();
      }
      }

      Output:

      1
      2
      3

      1
      1
      1

      1) A1 a = new A1(); does not make a thread. It’s just another class with no extra behavior. If you call A1.run() then it is not new thread. And A1.start() is not available to this class.
      2) Only way to create start a thread in java is calling it’s start method.
      3) If you look at constructor of Thread class, no constructor takes parameter of Thread class itself. And we know that Thread implements Runnable, effectively any call such as “new Thread(a).start();” is starting a thread with runnable mechanism.
      4) Correct way to start thread (“using extend”) is calling it’s start method only directly.
      e.g. new B1().start();
      5) So effectively, in both techniques in your code, you are doing the same thing i.e. implementing runnable.

      • By extending Thread, each of your threads has a unique object associated with it, whereas implementing Runnable, many threads can share the same runnable instance.

        The statement is alright but what is the advantage we gain here ?

      • The code is misleading. It doesn’t match the intent. It’s true that Thread class itself implements Runnable interface, so any instance of a class extending the Thread class can be passed in to creating new thread.

        ExtendsThread et = new ExtendsThread(); 
        Thread tc1 = new Thread(et); tc1.start(); 
        Thread tc2 = new Thread(et); tc2.start();

        These two threads share the same instance.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.