Java Thread vs Runnable: Which to Use
java thread vs runnable: Compare Java's Thread class and Runnable interface for defining concurrent tasks, and learn which approach fits different scenarios.
When you need to run code in a separate thread in Java, you have two standard options: extend the Thread class or implement the Runnable interface. The choice between java thread vs runnable affects how you structure your task, how you reuse it, and how you integrate it with modern concurrency utilities. This article explains the technical difference, shows both implementations, and gives concrete guidance for selecting the right one.
The Core Difference: Inheritance vs Composition
Thread is a class that represents an actual thread of execution. Runnable is an interface that defines a single method, run(), which contains the code that should execute in a thread. When you extend Thread, you are creating a new type that is a thread. When you implement Runnable, you are creating a task that can be run by a thread, but the task itself is not a thread.
This distinction matters because Java supports single inheritance for classes. If your class already extends another class, it cannot extend Thread. Implementing Runnable avoids that limitation entirely. It also separates the task definition from the execution mechanism, which leads to more flexible and testable code.
How to Use Runnable with Thread
The most common way to use Runnable is to pass an instance to a Thread constructor. The thread then invokes the run() method when it starts.
public class Task implements Runnable { @Override public void run() { System.out.println("Task is running in " + Thread.currentThread().getName()); } } Thread thread = new Thread(new Task()); thread.start();
The start() method launches a new thread that executes the run() method. The code inside run() runs concurrently with the calling thread. Because Task only implements Runnable, you can reuse the same instance in multiple threads if the task is stateless or properly synchronized.
With Java 8 and later, you can use a lambda expression to define a Runnable without creating a separate class:
Thread thread = new Thread(() -> System.out.println("Lambda task")); thread.start();
This is concise and works well for small tasks that do not need a dedicated class.
How to Extend Thread
Extending Thread means creating a subclass and overriding its run() method. The subclass itself is a thread, so you start it by calling start() on an instance of that subclass.
public class MyThread extends Thread { @Override public void run() { System.out.println("MyThread is running"); } } MyThread thread = new MyThread(); thread.start();
Here, MyThread is both the task and the thread. This approach is straightforward for simple cases, but it couples the task logic to the thread lifecycle. If you need to run the same task on multiple threads, you must create a new instance of MyThread for each thread, even if the task itself is identical.
Why Runnable Is Usually the Better Choice
Implementing Runnable offers several practical advantages over extending Thread.
First, it decouples the task from the thread. You can pass the same Runnable instance to a Thread, an ExecutorService, or a ForkJoinTask. This makes it easier to switch execution strategies without changing the task code.
Second, it preserves the ability to extend another class. Since Java does not support multiple inheritance, a class that already extends a base class cannot extend Thread. Implementing Runnable is the only option in that scenario.
Third, testing is simpler. A Runnable can be invoked directly by calling its run() method in a unit test, without dealing with thread scheduling or timing issues. In contrast, testing a Thread subclass often requires careful coordination to avoid flaky tests.
Finally, Runnable works naturally with the ExecutorService framework, which is the preferred way to manage threads in production code. Executors separate task submission from thread management and provide thread pooling, which avoids the overhead of creating a new thread for every task.
ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> System.out.println("Task via executor")); executor.shutdown();
The same Runnable can be submitted to an executor without any changes.
When Extending Thread Makes Sense
There are rare cases where extending Thread is justified. If you need to override methods other than run(), such as interrupt() or setName(), and you want to customize the thread's behavior at the class level, extending Thread gives you direct access. However, most of these customizations can be achieved by configuring a Thread instance before starting it, so even then Runnable is often sufficient.
Another scenario is when you are working with a legacy codebase that already defines tasks as Thread subclasses. Refactoring to Runnable may be worthwhile, but it is not always necessary if the existing code is stable and tested.
For new code, the Java documentation and community consensus strongly favor Runnable over extending Thread. The Thread class itself implements Runnable, so the distinction is not about functionality but about design.
Thread Safety and Shared State
The choice between Thread and Runnable does not change the fundamental concurrency rules. In both cases, the run() method executes in a separate thread, and any shared mutable state must be protected with synchronization, volatile variables, or atomic classes. Failing to do so can lead to race conditions, regardless of whether you used Thread or Runnable.
A common mistake is to call run() directly instead of start(). Calling run() executes the method in the current thread, not in a new one. This is easy to do accidentally, especially when you are used to invoking methods directly. Always use start() to launch a new thread.
Another pitfall is sharing a Runnable instance that has mutable instance variables across multiple threads without synchronization. If the run() method modifies a field, that field becomes shared state. You must either make the task stateless or synchronize access to the field.
Performance and Resource Considerations
Creating a new Thread for each task is expensive because it involves allocating a native thread stack and registering the thread with the operating system. This overhead is significant if you need to run many short-lived tasks. The ExecutorService framework reuses threads from a pool, which reduces this cost. Since Runnable works directly with executors, it is the natural fit for high-throughput concurrent applications.
Extending Thread does not inherently change performance, but it encourages the pattern of creating a thread per task, which is often inefficient. By using Runnable and an executor, you can control the number of threads and avoid resource exhaustion.
There is also a memory consideration. Each thread has its own stack, and the default stack size can be large. If you create thousands of threads, you may run out of memory. A thread pool with a bounded size prevents this problem. Runnable tasks are lightweight objects, so they do not add significant memory overhead themselves.
Choosing Between Thread and Runnable in Modern Java
In modern Java, the Runnable interface is the standard way to define a task. It is a functional interface, so it works with lambdas and method references. The Thread class is still present for low-level control, but the ExecutorService and CompletableFuture APIs build on Runnable and Callable to provide higher-level abstractions.
When you are starting a new project or adding concurrency to existing code, prefer Runnable (or Callable if you need a result) over extending Thread. The separation of task and execution gives you more flexibility and makes your code easier to test and maintain.
If you are maintaining legacy code that extends Thread, consider refactoring it to use Runnable when you need to add features or fix bugs. The change is usually mechanical: replace extends Thread with implements Runnable, move the run() method unchanged, and pass the instance to a Thread or executor. This reduces coupling and prepares the code for better concurrency management.
Ultimately, the java thread vs runnable decision is about design intent. Thread is a concrete execution entity; Runnable is a reusable unit of work. Choosing Runnable keeps your concurrency code modular and aligned with the evolution of Java's concurrency APIs.