Java Runnable Interface

In the last lesson you learned about the Java Thread class. That works, but it uses up your one allowed parent class. The more flexible way to define a thread’s work is the Runnable interface, and it is what you will see most in real code.

πŸ€” Why Runnable instead of extending Thread?

Think of a recipe and a cook. The recipe describes what to make. The cook makes it. You keep them separate, so any cook can follow any recipe. Java threading works the same way. The task is the recipe. The thread is the cook. Extending Thread glues them into one object, which causes a real problem:

  • A Java class can extend only one class. That is single inheritance.
  • If your class extends Thread, that one slot is used up.
  • If your class needs to extend something else, like a framework base class, you are stuck.

This snippet shows the trap.

// ❌ this class can't extend anything else now
class Downloader extends Thread {
// What if Downloader also needs to extend FileWorker?
// It can't. The single inheritance slot is already used by Thread.
public void run() {
System.out.println("Downloading...");
}
}

Runnable removes this problem. Your class implements the Runnable interface instead of being a thread. A class can implement many interfaces while still extending one class, so your inheritance slot stays free. The same recipe can be handed to many cooks.

This version is free to extend whatever it wants.

// βœ… implements Runnable, so the extends slot is still open
class Downloader extends FileWorker implements Runnable {
public void run() {
System.out.println("Downloading...");
}
}

🧩 What is Runnable?

Runnable is a functional interface with a single method, run(). A functional interface has exactly one method you must fill in. Things to hold in your head:

  • It represents a task, a piece of work you want done.
  • You write the work inside run(), which takes no arguments and returns nothing (void).
  • A Runnable does nothing on its own. A thread has to run it.
  • The Runnable is what to do; the Thread is who runs it.

The interface itself is tiny. It really is just this one method.

// this is the whole Runnable interface, more or less
interface Runnable {
void run();
}

You give your task to a worker thread, and the worker runs it. This lines up with how the rest of Java’s concurrency tools are built.

πŸ› οΈ Using Runnable

Using Runnable follows the same repeatable steps every time:

  • Write a class that implements Runnable.
  • Put your work inside run().
  • Create an instance of that class.
  • Pass it to a new Thread(...).
  • Call start() on the thread.

Here is the full pattern in one program.

class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("Task says: " + i);
}
}
}
public class Main {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task); // give the task to a thread
thread.start(); // the thread runs the task
System.out.println("Main keeps going");
}
}

Let’s walk through what each part does:

  • MyTask implements Runnable and defines the work inside run().
  • We create the task with new MyTask().
  • We wrap it in new Thread(task), which hands the task to a worker thread.
  • thread.start() tells the worker to begin, on a separate path of execution.
  • Meanwhile main keeps going and prints its own line.

Notice the class did not extend Thread, so MyTask stays free to extend something else. And start() is what creates the new thread. We never call run() ourselves.

Output (order may vary)

Main keeps going
Task says: 1
Task says: 2
Task says: 3

The order can change between runs. The main thread and the task thread run at the same time, so who prints first depends on the scheduler. Never assume an order unless you add coordination.

⚑ The lambda shortcut

Because Runnable is a functional interface with one method, you can write it as a lambda instead of a whole class. A lambda is a short way to write a small piece of code you pass around. This is the most common way you see Runnable in modern Java. Since Runnable has exactly one method, the lambda body simply becomes the body of run().

This example writes the same task as a lambda, with no separate class.

public class Main {
public static void main(String[] args) {
// the task as a lambda, no separate class needed
Runnable task = () -> {
for (int i = 1; i <= 3; i++) {
System.out.println("Lambda task: " + i);
}
};
Thread thread = new Thread(task);
thread.start();
// or even shorter, inline:
new Thread(() -> System.out.println("Quick task")).start();
}
}

Here is how to read this:

  • The lambda () -> { ... } is the run() method. The empty () matches run() taking no arguments.
  • We store it in a Runnable variable, then pass that to a Thread.
  • The last line shows the most compact form. We create a thread with an inline lambda and start it in one line.

The arrow -> separates the parameters from the body. So () -> System.out.println("Quick task") means β€œno inputs, do this print.” This compact style is clean and very common.

Output (order may vary)

Lambda task: 1
Lambda task: 2
Lambda task: 3
Quick task

πŸ” Worked example: one Runnable, two threads

Because a Runnable is just a task, you can hand the same Runnable to more than one thread. Two cooks, one recipe. Each thread runs its own copy of the work.

This program creates one PrintTask and gives it to two different threads.

class PrintTask implements Runnable {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 1; i <= 3; i++) {
System.out.println(name + " -> " + i);
}
}
}
public class Main {
public static void main(String[] args) {
PrintTask task = new PrintTask(); // one task
Thread alex = new Thread(task, "Alex"); // worker 1
Thread riya = new Thread(task, "Riya"); // worker 2
alex.start();
riya.start();
}
}

Let’s break down the new parts:

  • Thread.currentThread().getName() asks the thread for its own name, so we can see which worker is printing.
  • We pass a name as the second argument, new Thread(task, "Alex"), to label each thread.
  • The same task object goes to both alex and riya.
  • Both call start(), so both run at the same time.

Output (order may vary)

Alex -> 1
Riya -> 1
Alex -> 2
Alex -> 3
Riya -> 2
Riya -> 3

Two important lessons from this output:

  • The lines from Alex and Riya are mixed together, because both threads run at the same time.
  • The order is not fixed. Run it again and you may get a different mix.

One task, two workers running it together. You could not do this as cleanly if the task and the thread were the same object.

πŸ†š Runnable vs Callable

Callable is a cousin of Runnable. The short version:

  • Runnable has run(). It returns nothing and cannot throw a checked exception. Use it when the task just does something, like printing or saving a file.
  • Callable has call(). It can return a value and can throw a checked exception. Use it when the task hands back a result, like a computed number.

Callable has its own lesson. For now, just remember: Runnable does work; Callable does work and hands back an answer.

βš–οΈ Runnable vs Thread

Both let you run code on a new thread. The quick comparison:

  • Extend Thread: simpler to type, but uses up your inheritance slot, mixes the task with the thread, and does not fit thread pools.
  • Implement Runnable: keeps inheritance free, separates task from thread, can be shared across threads, and works with the executor framework and thread pools.

For almost all real work, use Runnable. Extending Thread is fine only for a tiny throwaway example.

⚠️ Common Mistakes

A few Runnable slip-ups trip up almost everyone at first. Here is how to spot and fix them.

Calling run() directly. This is the classic one. Calling run() yourself does not start a new thread. It just runs the method on the current thread, like any normal method call.

Runnable task = () -> System.out.println(Thread.currentThread().getName());
// ❌ runs on the SAME thread, no new thread is created
task.run();
// βœ… creates a NEW thread and runs the task there
new Thread(task).start();

Forgetting to wrap it in a Thread. A Runnable on its own does nothing in parallel. It is just an object holding a task. It needs a Thread, or an executor, to actually run.

Runnable task = () -> System.out.println("work");
// ❌ nothing runs in parallel; you only made an object
// task;
// βœ… hand it to a thread so it actually runs
new Thread(task).start();

Sharing mutable state without synchronization. When two threads share the same Runnable that changes the same data, they can step on each other and produce wrong results. This is called a race condition.

class Counter implements Runnable {
int count = 0; // shared by both threads
public void run() {
for (int i = 0; i < 1000; i++) {
count++; // ❌ two threads changing count at once = wrong total
}
}
}
// βœ… protect shared data with synchronization (next lesson) before sharing it

Extending Thread out of habit. It is tempting because it looks shorter. Prefer Runnable so you keep your inheritance free and your design clean. The few extra characters pay off quickly.

βœ… Best Practices

These habits will keep your threaded code clean and correct.

  • Prefer Runnable over extending Thread. It is more flexible and is the standard approach across real codebases.
  • Use a lambda for short tasks. Since Runnable has one method, a lambda is concise and easy to read.
  • Keep each task focused. A Runnable should describe one clear unit of work, not five unrelated things.
  • Pass the Runnable to a Thread and call start(). That is how the task actually runs in parallel. Never call run() yourself.
  • Reach for executors and thread pools as you grow. Creating raw threads by hand is fine for learning. For real apps, the executor framework manages threads for you, and it takes Runnables directly.

Runnable is the recommended way

Prefer implementing Runnable, or using a lambda, over extending Thread. It keeps your class free to extend something else, separates the task from the thread, lets the same task run on many threads, and works smoothly with the executor framework you will meet soon.

🧩 What You’ve Learned

Nicely done. Let’s recap Runnable.

  • βœ… Runnable is a functional interface with one method, run, that describes a task.
  • βœ… It is preferred over extending Thread because it keeps your inheritance free and separates the task from the thread.
  • βœ… You pass a Runnable to a Thread and call start() to run it in parallel.
  • βœ… The same Runnable can be handed to many threads, since it is just a task.
  • βœ… Because it is a functional interface, you can write a Runnable as a lambda.
  • βœ… Callable is the cousin that returns a value; you will study it later.
  • βœ… Use Runnable for real code; extending Thread is only for tiny examples.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does the Runnable interface represent?

    Why: Runnable describes the work (task); a Thread runs it.

  2. 2

    Why prefer Runnable over extending Thread?

    Why: Implementing Runnable leaves your class free to extend something else and is cleaner design.

  3. 3

    How do you run a Runnable in parallel?

    Why: Wrap the Runnable in a Thread and call start() to run it on a new thread.

  4. 4

    Why can a Runnable be written as a lambda?

    Why: Runnable has a single method (run), so a lambda can provide its body.

πŸš€ What’s Next?

A thread moves through several stages during its life, from new to running to dead. Knowing these stages helps you reason about what your threads are doing. Let’s walk through the thread lifecycle next.

Java Thread Lifecycle

Share & Connect