By WebNest Studio Engineering Team
Java - Core Tutorial
Creating Threads: Thread Class vs Runnable
Java gives you two standard ways to define the code a thread should run: extending the Thread class and overriding run(), or implementing the Runnable interface and passing it to a Thread. Both approaches ultimately start a new thread the same way — by calling start() — but they differ in flexibility and design implications.
Why it matters
There are two ways to define what a thread does, and one mistake that makes neither of them work: calling run() instead of start(). The code runs, prints the right thing, and no second thread was ever created. Knowing the difference, and why Runnable is preferred, is basic to every later topic in concurrency.
Approach 1: Extending Thread
You create a subclass of Thread and override its run() method with the code you want executed concurrently. You then instantiate your subclass and call start() on it. This works, but it consumes your one allowed superclass in Java — since Java does not support multiple inheritance of classes, a class that extends Thread cannot extend anything else.
Approach 2: Implementing Runnable (Preferred)
You create a class that implements Runnable and defines its single abstract method, run(). You then wrap an instance of that class in a new Thread(runnableInstance) and call start() on the Thread object. This is generally the preferred approach because your task class remains free to extend some other class if needed, it cleanly separates "the task to run" from "the mechanism that runs it," and it works naturally with the Executor framework, which expects Runnable (or Callable) tasks rather than Thread subclasses.
- Extends Thread: simpler for a one-off, but uses up your single inheritance slot.
- Implements Runnable: more flexible, reusable across multiple Thread objects, and the idiomatic choice in modern Java code, including with lambdas.
start() vs run() — The Most Important Distinction
Calling start() asks the JVM to create a new call stack and a new native OS thread, which will eventually invoke run() on that new thread. Calling run() directly, by contrast, is just an ordinary method call — it executes on the current thread, synchronously, with no concurrency at all. This is one of the most common beginner mistakes: the code compiles and even produces output, but nothing actually runs concurrently.
Example
Both approaches producing a concurrently running thread
class CounterThread extends Thread {
@Override
public void run() {
System.out.println("CounterThread running on: " + Thread.currentThread().getName());
}
}
class CounterTask implements Runnable {
@Override
public void run() {
System.out.println("CounterTask running on: " + Thread.currentThread().getName());
}
}
public class ThreadCreationDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new CounterThread();
t1.setName("Worker-Extends");
t1.start();
t1.join();
Thread t2 = new Thread(new CounterTask(), "Worker-Runnable");
t2.start();
t2.join();
// run() called directly: executes on main, NOT a new thread
new CounterTask().run();
}
}
CounterThread running on: Worker-Extends
CounterTask running on: Worker-Runnable
CounterTask running on: main
Common Mistakes
- Calling run() instead of start() and expecting concurrent execution — run() just executes synchronously on the calling thread.
- Extending Thread unnecessarily when the class already needs to extend something else, or when the task should be reusable across multiple threads.
- Forgetting that each Thread object can only be started once — attempting start() a second time throws IllegalThreadStateException.
- Not naming threads, which makes log output and thread dumps ("Thread-0", "Thread-1"...) hard to interpret during debugging.
Exercise
Create a thread from a Runnable written as a lambda. The thread prints worker running. The main thread waits for it and then prints main done.
Expected output: worker running then main done
public class FirstThread {
public static void main(String[] args) throws InterruptedException {
// TODO: create a Runnable that prints "worker running"
// TODO: create a Thread for it, start it, and wait for it to finish
System.out.println("main done");
}
}
Show hints
- A Runnable has one method with no arguments, so it can be written as
() -> .... - Without
join(), the two lines could appear in either order.
Show solution
public class FirstThread {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> System.out.println("worker running");
Thread worker = new Thread(task);
worker.start();
worker.join();
System.out.println("main done");
}
}
Quick quiz
1. What happens when you call thread.run() directly instead of thread.start()?
- A. A new thread runs the task
- B. It throws an exception
- C. It does not compile
- D. The task runs on the current thread; no new thread is created
Show answer
D. run() is an ordinary method call. Only start() asks the JVM to create a new thread, which then calls run().
2. Why is implementing Runnable generally preferred over extending Thread?
- A. The class stays free to extend another class, and the task is separate from the thread that runs it
- B. It runs faster
- C. Runnable threads cannot fail
- D. It uses no memory
Show answer
A. Java allows one superclass, and a Runnable can also be handed to a thread pool rather than tied to one thread.
3. What happens if start() is called twice on the same Thread object?
- A. The task runs twice
- B. It throws IllegalThreadStateException
- C. The second call is ignored
- D. It does not compile
Show answer
B. A Thread object can be started only once.
Interview questions
What are the ways to create a thread in Java?
Show answer
Extend Thread and override run(), or implement Runnable (often as a lambda) and pass it to a Thread. A third form is Callable, which returns a result and is run through an ExecutorService. In real applications you rarely create threads directly; you submit tasks to a thread pool.
What is the difference between start() and run()?
Show answer
start() creates a new thread of execution and returns immediately; the new thread then calls run(). Calling run() yourself executes the method on the calling thread, like any other method, with no concurrency at all.
Key Points to Remember
- Extending Thread and implementing Runnable both let you define a thread's work, but Runnable is generally preferred because Java has no multiple inheritance.
- Runnable separates the task from the execution mechanism and integrates cleanly with the Executor framework.
- start() launches a new thread and eventually calls run() on it; calling run() directly is just a normal, synchronous method call.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering