Question 26 · Section 9

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...

Language versions: English Russian Ukrainian

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 trace
  • Runnable — NOT visible separately, it “dissolves” inside the stack frame of the run() method

Best Practices

  1. Implement Runnable — don’t inherit from Thread
  2. Use ExecutorService — don’t create Thread directly
  3. Call start() — not run() to launch a thread
  4. Set UncaughtExceptionHandler — for error handling
  5. Name threads — custom ThreadFactory for debugging
  6. Thread Context ClassLoader — for dynamic class loading
  7. Don’t use ThreadGroup — ExecutorService or StructuredTaskScope
  8. 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 of Runnable (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]]