skip to content

What are the two main ways to define the work a thread runs in Java, and how do they differ?

level: juniorimportance: must knowfreq 78%

answer

  1. Two ways: extend Thread vs implement Runnable
  2. Both define the work in run()
  3. Thread = is-a; Runnable = has-a (composition)
  4. Single inheritance means extends Thread spends your one slot
  5. Runnable can be a lambda and fed to a thread pool

basics

~20 s

You can extend the Thread class and override its run() method, or implement the Runnable interface's run() method and hand it to a Thread. Both put your code in run(); Runnable just separates the task from the thread.

solid answer

~40 s

Java offers two ways to define a thread's work. First, extend java.lang.Thread and override run(), then call start() on your subclass. Second, implement the Runnable interface (a single run() method holding the task), pass the Runnable to a Thread constructor, and call start() on that Thread. In both cases the JVM runs your run() body on a fresh thread. The difference is structural: extending Thread couples your task to the thread mechanism and uses up your one inheritance slot, while Runnable keeps the task as a separate, reusable object you can pass to a plain Thread or, better, to an ExecutorService thread pool. Modern code almost always uses Runnable (often as a lambda) for that flexibility.

go deeper

for a junior

Name both options and state that the work goes in run(); know that Runnable is the usual choice.

for a middle

Explain the is-a vs has-a / composition argument and that single inheritance makes extends Thread restrictive; show the lambda form.

for a senior

Tie Runnable to executor frameworks and testability; explain that decoupling task from execution is what makes pooling and reuse possible.

for a principal

Frame the choice as 'separate policy (how/when to run) from mechanism (what to run)', enabling pluggable execution strategies, structured concurrency, and virtual threads without rewriting tasks.

## What a thread is A **thread** is an independent path of execution inside a program. A normal program has one thread (the 'main' thread) that runs your code top to bottom. A second thread lets two pieces of code make progress at the same time (on multi-core hardware, literally in parallel). To create one, you must tell the thread *what work to run*. Java gives two ways to supply that work. ## Way 1 — extend Thread `java.lang.Thread` is a class that represents a thread of execution. It has a `run()` method which, by default, does nothing. You can make a subclass and **override** `run()` with your task: ```java class Worker extends Thread { @Override public void run() { System.out.println("working"); } } new Worker().start(); ``` Calling `start()` asks the JVM to spawn a new OS thread and invoke your `run()` on it. ## Way 2 — implement Runnable `java.lang.Runnable` is a **functional interface** (an interface with exactly one abstract method) declaring just `void run()`. You write a class (or lambda) that implements `run()`, then give that object to a `Thread`: ```java Runnable task = () -> System.out.println("working"); Thread t = new Thread(task); t.start(); ``` The `Thread` holds your `Runnable` and, when started, calls its `run()` on the new thread. ## The key difference: inheritance vs. composition - **extends Thread** *is-a* thread — your class becomes a thread. Java allows only **single inheritance** (a class can extend exactly one class), so subclassing Thread spends that one slot; you can no longer extend anything else. - **implements Runnable** *has-a* task — the task is a plain object, and the Thread merely runs it (**composition**). The task stays decoupled from how it's executed, so the same Runnable can run on a raw Thread, be submitted to a thread pool (ExecutorService), reused, or unit-tested as an ordinary object. ## Why both call run() In both designs the executable code lives in a method named `run()`. The difference is only *who owns* that method: the Thread subclass itself, or a separate Runnable that the Thread delegates to. Internally, `Thread.run()` (when you pass a Runnable) simply calls `target.run()`. ## What to remember Extending Thread is the older, more rigid style. Implementing Runnable (today usually a lambda) is the idiomatic choice because it separates *what to do* from *how to run it*, plays nicely with thread pools, and leaves your inheritance slot free.

  • If both put the code in run(), why does Runnable matter at all?
    Because Runnable separates the task object from the thread that runs it. That lets you reuse the task, hand it to an ExecutorService pool, mock it in tests, and keep your single inheritance slot free — none of which extending Thread allows.
  • Can you write a Runnable as a lambda?
    Yes. Runnable is a functional interface (single abstract method run()), so `Runnable r = () -> doWork();` compiles, and `new Thread(() -> doWork()).start()` is idiomatic.

Think of a thread as a worker (the engine) and the task as a job sheet. 'extends Thread' welds the job sheet onto one specific worker. 'implements Runnable' writes the job on a card any worker can pick up — including a whole crew (thread pool).

saying these in an interview costs you the question

  • Saying Runnable runs the task itself — Runnable is just the task; a Thread (or executor) actually runs it.
  • Claiming extending Thread is faster or 'creates the thread better' — both produce the same kind of thread; the difference is design, not performance.
  • Thinking you must choose Thread to get concurrency — concurrency comes from start()/the executor, not from which option you pick.

context