What is the difference between Thread and Runnable?
Thread and Runnable are two fundamentally different concepts in Java that are often confused. Runnable describes a task ("what to do"), while Thread is the executor ("who execut...
Thread and Runnable are two fundamentally different concepts in Java that are often confused. Runnable describes a task (“what to do”), while Thread is the executor (“who executes”). This separation is an example of the Command pattern: the command object (Runnable) is separate from the executor object (Thread).
Why this separation matters: if the task were tied to the executor (as with inheriting from Thread), you couldn’t reuse the same task in different contexts (thread pool, Virtual Thread, current thread). Separation provides flexibility.
Junior Level
Basic Understanding
Thread and Runnable are two different things in Java:
| Concept | What It Is | Role |
|---|---|---|
| Runnable | Interface (interface) |
Task — “what to do” |
| Thread | Class (class) |
Carrier — “who executes” |
Runnable — this is just a block of code that can be executed. By itself, it doesn’t create a thread. Thread — this is a wrapper over an operating system thread, which takes a Runnable and runs it in a separate execution thread.
Simple Analogy
- Runnable = recipe (description of what to cook)
- Thread = cook (who cooks according to the recipe)
Runnable Example
// Runnable — this is just a task
Runnable task = () -> {
System.out.println("Executing task!");
};
Thread Example
// Thread — this is the executor
Thread thread = new Thread(task); // Pass the task
thread.start(); // Launch a new thread
Full Comparison
| Characteristic | Runnable | Thread |
|---|---|---|
| Type | Interface | Class |
| Method | run() |
start(), join(), interrupt(), … |
| Creates a thread? | No | Yes (when start() is called) |
| Memory | Object in heap (~24 bytes) | Object + native stack (~1MB) |
| Reusable? | Yes | No (one Thread = one execution) |
Common Mistake: run() vs start()
Runnable task = () -> System.out.println("Thread: " + Thread.currentThread().getName());
// BAD: run() — executes in the CURRENT thread
task.run(); // "Thread: main" — NOT a new thread!
// GOOD: start() — creates a NEW thread
Thread thread = new Thread(task);
thread.start(); // "Thread: Thread-0" — new thread!
Middle Level
Composition over Inheritance
// BAD: inheriting from Thread
class MyTask extends Thread {
@Override
public void run() {
// Now MyTask CANNOT inherit from anything else
}
}
// GOOD: implementing Runnable
class MyTask implements Runnable {
@Override
public void run() {
// MyTask can still inherit from another class
}
}
Why Runnable Is Better?
| Criterion | extends Thread | implements Runnable |
|---|---|---|
| Multiple inheritance | No (occupied by Thread) | Yes |
| Reusability | No (one Thread) | Yes (in any ExecutorService) |
| Separation of concerns | No (task + carrier) | Yes (task separate) |
Passing Runnable to Different Executors
Runnable task = () -> System.out.println("Working...");
// 1. In Thread
new Thread(task).start();
// 2. In ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(task);
// 3. In ForkJoinPool
ForkJoinPool.commonPool().execute(task);
// 4. Just in the current thread
task.run();
Thread Composition (What’s Inside)
Thread thread = new Thread(task);
// Thread contains:
thread.getId(); // Unique ID
thread.getName(); // Name ("Thread-0")
thread.getPriority(); // Priority (1-10)
thread.isDaemon(); // Daemon or not
thread.getContextClassLoader(); // ClassLoader for frameworks
thread.getState(); // NEW, RUNNABLE, BLOCKED, WAITING, ...
Thread States
| State | Description |
|---|---|
| NEW | Created, but start() not called |
| RUNNABLE | Executing or ready to execute |
| BLOCKED | Waiting for monitor release (synchronized) |
| WAITING | Waiting for notify() or park() |
| TIMED_WAITING | Waiting with timeout (sleep, wait(timeout)) |
| TERMINATED | Finished |
Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (or TERMINATED if fast)
t.join();
System.out.println(t.getState()); // TERMINATED
Senior Level
Under the Hood: Thread Creation
// On calling start():
thread.start();
// JVM does:
// 1. Calls native method:
// - Linux: pthread_create()
// - Windows: CreateThread()
// 2. OS allocates:
// - Stack (default 1MB)
// - Thread-local storage
// - Entries in the scheduler
// 3. Adds to OS scheduler queue
// 4. Only then calls Runnable.run()
Stack Memory
1000 Runnable objects: ~24 KB (just objects in Young Gen)
1000 Thread objects: ~1 GB (1000 x 1MB native stacks)
Thread Context ClassLoader
// Frameworks use it for class loading
Thread.currentThread().setContextClassLoader(myClassLoader);
// Hibernate, Spring and others use:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class<?> clazz = cl.loadClass("com.example.MyEntity");
Priority — Myth and Reality
Thread t = new Thread(task);
t.setPriority(Thread.MAX_PRIORITY); // 10
// On modern OSes (Linux CFS) Java priorities
// are mostly IGNORED by the scheduler!
// OS uses its own complex fairness algorithms
UncaughtExceptionHandler
Thread t = new Thread(() -> {
throw new RuntimeException("Uncaught error!");
});
// Without handler — error just goes to System.err
t.setUncaughtExceptionHandler((thread, ex) -> {
System.err.println("Thread " + thread.getName() + " crashed: " + ex);
// Logging, alert, restart
});
t.start();
Thread Groups — Deprecated Concept
// ThreadGroup — NOT recommended
ThreadGroup group = new ThreadGroup("my-group");
new Thread(group, task).start();
// Nowadays for grouping use:
// 1. ExecutorService
// 2. StructuredTaskScope (Java 21+)
// 3. Custom ThreadFactory with names
Diagnostics
jstack
jstack <pid>
"main" #1 prio=5 os_prio=0 tid=0x00007f... nid=0x1234 runnable
at com.example.MyClass.myMethod(MyClass.java:42)
"worker-1" #10 prio=5 os_prio=0 tid=0x00007f... nid=0x1235 waiting on condition
- parking to wait for <0x000000076af0c8d0>
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
Thread Dumps — What’s Visible
Thread— visible: state, name, stack traceRunnable— NOT visible separately, it “dissolves” inside the stack frame of therun()method
Best Practices
- Implement Runnable — don’t inherit from Thread
- Use ExecutorService — don’t create Thread directly
- Call start() — not run() to launch a thread
- Set UncaughtExceptionHandler — for error handling
- Name threads — custom ThreadFactory for debugging
- Thread Context ClassLoader — for dynamic class loading
- Don’t use ThreadGroup — ExecutorService or StructuredTaskScope
- Don’t rely on priorities — OS ignores them
When NOT to Use Runnable
- Single-threaded task — if code runs in only one thread, Runnable adds unnecessary abstraction
- Need a result — use
Callable<V>instead ofRunnable(Runnable.run() returns void) - Need to throw a checked exception — Runnable.run() doesn’t declare throws, use Callable
When NOT to Create Thread Directly
- ExecutorService is available — thread pool is more efficient (reuses threads, manages queue)
- Virtual Threads available (Java 21+) — for I/O-bound tasks
Thread.ofVirtual()is simpler and lighter - StructuredTaskScope (Java 21 Preview) — for groups of related tasks with shared lifecycle
Runnable vs Callable vs Thread: What to Choose?
| Situation | Choice | Why |
|---|---|---|
| Task without result (logging, send) | Runnable | Simple functional interface |
| Task with result | Callable |
Returns V, can throw checked exception |
| Quick prototype (1-2 threads) | new Thread(runnable) | Minimum code |
| Production (pool, management) | ExecutorService.submit(runnable) | Thread reuse, queue |
| I/O-bound, many tasks (Java 21+) | Thread.ofVirtual().start(runnable) | Millions of lightweight threads |
Interview Cheat Sheet
Must know:
- Runnable — interface (task, “what to do”), Thread — class (executor, “who executes”)
- Runnable.run() executes in the CURRENT thread, Thread.start() creates a NEW thread
- Composition over Inheritance: implements Runnable is better than extends Thread (flexibility, thread pools, Virtual Threads)
- 1000 Runnable objects = ~24 KB, 1000 Thread = ~1 GB (native stacks)
- Thread States: NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED
- Thread priority on modern OSes (Linux CFS) is mostly ignored by the scheduler
Frequent follow-up questions:
- Why shouldn’t you inherit from Thread? — You can, but it’s an anti-pattern: lose multiple inheritance, can’t reuse in thread pools
- Why is Context ClassLoader needed in Thread? — Frameworks (Hibernate, Spring) use it for dynamic class loading from the correct classpath
- Why is ThreadGroup deprecated? — Instead, use ExecutorService, StructuredTaskScope (Java 21+), or custom ThreadFactory
- What to do with uncaught exceptions in Thread? —
setUncaughtExceptionHandler()— otherwise error only goes to System.err
Red flags (DO NOT say):
- “Thread and Runnable are the same thing” — Runnable is the task, Thread is the executor
- “Calling run() launches a new thread” — run() executes in the current thread, need start()
- “Thread priority guarantees execution order” — modern OSes ignore Java priorities
- “ThreadGroup is a good way to group threads” — deprecated, use ExecutorService or StructuredTaskScope
Related topics:
- [[19. What conditions are necessary for deadlock to occur]]
- [[21. What is race condition]]
- [[23. What are Virtual Threads in Java 21]]
- [[28. What are Callable and Future]]