Java Runnable Interface
Table of Contents + β
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
extendonly 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 nowclass 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 openclass 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 lessinterface 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:
MyTaskimplementsRunnableand defines the work insiderun().- 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
mainkeeps 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 goingTask says: 1Task says: 2Task says: 3The 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 therun()method. The empty()matchesrun()taking no arguments. - We store it in a
Runnablevariable, then pass that to aThread. - 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: 1Lambda task: 2Lambda task: 3Quick 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
taskobject goes to bothalexandriya. - Both call
start(), so both run at the same time.
Output (order may vary)
Alex -> 1Riya -> 1Alex -> 2Alex -> 3Riya -> 2Riya -> 3Two 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 createdtask.run();
// β
creates a NEW thread and runs the task therenew 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 runsnew 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 itExtending 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 callrun()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
Threadand callstart()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
What does the Runnable interface represent?
Why: Runnable describes the work (task); a Thread runs it.
- 2
Why prefer Runnable over extending Thread?
Why: Implementing Runnable leaves your class free to extend something else and is cleaner design.
- 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
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.