What is the difference between Thread and Runnable
The difference between Thread and Runnable stems from a fundamental separation of concerns following the Command Pattern: Runnable represents the task ("what to do"), whereas Th...
🟢 Junior Level
Short Interview Answer (30 Seconds)
The difference between Thread and Runnable stems from a fundamental separation of concerns following the Command Pattern: Runnable represents the task (“what to do”), whereas Thread represents the execution context and worker (“who executes it and in what environment”).
Runnable is a functional interface with a single method void run(). It models an abstract block of executable code decoupled from any specific underlying thread.
Thread is a concrete runtime execution class that manages an operating system thread (or a virtual thread in Java 21), including its state machine, priority, call stack, and lifecycle. Inheriting from Thread rigidly couples task logic to thread management, whereas implementing Runnable allows the exact same task logic to run inside thread pools (ExecutorService), on virtual threads (Virtual Threads), or even synchronously on the calling thread.
Comparison Table: Thread vs. Runnable
| Characteristic | Runnable |
Thread |
|---|---|---|
| Entity Type | Functional interface (@FunctionalInterface) |
Concrete class (java.lang.Thread) |
| Architectural Role | Unit of work / Task | Execution carrier / Worker |
| Resource Allocation | Ordinary heap object (~24–32 bytes) | Heavyweight native OS stack (~1 MB via -Xss) + OS thread descriptor |
| Execution Method | run() (runs synchronously on the current thread) |
start() (spawns a new native OS thread via JNI) |
| Reusability | Unlimited (can be submitted repeatedly to pools) | Single-use (calling start() a second time throws an exception) |
| Inheritance | Leaves the single class inheritance slot available | Consumes Java’s single inheritance slot (extends) |
| Framework Integration | Industry standard for ExecutorService, Loom, Streams |
Low-level primitive; rarely instantiated directly in production |
Java 21 Examples: Correct Execution vs. Common Trap
import java.util.concurrent.Executors;
public class ThreadVsRunnableDemo {
public static void main(String[] args) throws Exception {
// 1. Task definition via Runnable lambda
Runnable printTask = () -> {
System.out.println("Executing task in thread: " + Thread.currentThread().getName());
};
// ❌ COMMON PITFALL: Directly calling run() does NOT start a new thread!
printTask.run(); // Prints: "Executing task in thread: main"
// ✅ Starting via a classic Platform Thread
Thread platformThread = new Thread(printTask, "platform-worker-1");
platformThread.start(); // Spawns an operating system thread
// ✅ Modern execution via Virtual Thread (Java 21)
Thread virtualThread = Thread.ofVirtual()
.name("virtual-worker-1")
.start(printTask);
// ✅ Production standard: submitting Runnable to an ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(printTask);
}
platformThread.join();
virtualThread.join();
}
}
🟡 Middle Level
Principle: “Composition over Inheritance”
In outdated tutorials, threads were often created by subclassing: class MyTask extends Thread. In modern enterprise software, this is considered an anti-pattern for three reasons:
- Violation of the Single Responsibility Principle (SRP): The class conflates business computation logic with low-level thread lifecycle management.
- Loss of Class Inheritance: Java does not support multiple class inheritance. Subclassing
Threadprevents the class from extending domain base classes (e.g.,BaseEntityService). - Incompatibility with Thread Pools:
ExecutorService.submit()acceptsRunnableorCallable. If task logic is hardcoded inside an extendedThread, thread pools cannot reuse their pre-warmed worker threads to execute it.
Thread Lifecycle (Thread.State)
A Java thread transitions through strictly defined states defined in the java.lang.Thread.State enum:
[ NEW ]
│ thread.start()
▼
[ RUNNABLE ] ◄────────────────────────┐
│ │ │
Waiting for│ │ wait() / join() / park() │ Notified / Lock acquired
synchronized │ ▼ │
monitor │ [ WAITING / TIMED_WAITING ] ─────┘
▼
[ BLOCKED ]
│
│ run() completes
▼
[ TERMINATED ]
NEW: TheThreadinstance is allocated in the heap, but its native kernel thread has not yet been allocated (start()has not been called).RUNNABLE: The thread is executing on a CPU core or waiting in the OS scheduler queue (Java combines Ready and Running into a single state).BLOCKED: The thread is suspended waiting to acquire anObjectMonitorfor asynchronizedblock or method.WAITING: The thread is suspended indefinitely awaiting an explicit signal from another thread (Object.wait(),Thread.join(),LockSupport.park()).TIMED_WAITING: The thread is suspended for a bounded duration (Thread.sleep(millis),Object.wait(timeout)).TERMINATED: Execution ofrun()has finished (either normally or abruptly due to an uncaught exception).
Handling Uncaught Exceptions: UncaughtExceptionHandler
The Runnable.run() method does not declare checked exceptions (throws Exception). If an unchecked exception (RuntimeException or Error) is thrown inside run(), it terminates the thread immediately:
public class ThreadExceptionHandlerDemo {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
throw new IllegalStateException("Critical business transaction failure!");
});
// 1. Thread-specific uncaught exception handler:
worker.setUncaughtExceptionHandler((thread, throwable) -> {
System.err.println("Thread " + thread.getName() + " crashed: " + throwable.getMessage());
// Forward alert to observability tools (Sentry, Prometheus, Datadog)
});
// 2. Global JVM-wide default uncaught exception handler:
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("Global JVM handler caught crash in thread: " + t.getName());
});
worker.start();
}
}
🔴 Senior Level
Under the Hood JVM HotSpot: Anatomy of thread.start()
Here is the exact sequence of events when thread.start() is invoked:
Java Code: thread.start()
│
▼ (Java Native Interface - JNI)
HotSpot C++: JVM_StartThread (jvm.cpp)
│
├─► Allocates C++ JavaThread structure
├─► Invokes os::create_thread (os_linux.cpp)
│ │
│ ▼ System Call (Linux Kernel)
│ pthread_create(..., &thread_native_entry, ...)
│ │
│ ├─► Reserves virtual memory stack (-Xss: 1 MB)
│ ├─► Allocates kernel OSThread descriptor
│ └─► Enqueues into Linux CFS scheduler
│
▼ HotSpot C++
Invokes thread_native_entry()
│
▼ Java Callback
Invokes target runnable.run()
- State Validation:
start()first inspects the internalthreadStatusfield. IfthreadStatus != 0(meaning the thread was started previously), it immediately throwsIllegalThreadStateException. Java threads are strictly single-use state machines. - State Transition: Following the
pthread_createsystem call, the OS kernel returns apthread_thandle. HotSpot transitions the thread state toRUNNABLEand yields control to the OS kernel scheduler.
Thread Priorities (Thread.setPriority) and the Linux CFS Scheduler
Thread provides setPriority(int priority) ranging from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY).
[!WARNING] In modern Linux server environments, calling
setPriority()is essentially a no-op:
The standard Linux scheduler—CFS (Completely Fair Scheduler) or EEVDF (Linux kernel 6.6+)—operates on the concept of virtual runtime (vruntime) andnicevalues (-20 to +19). Modifying thread priority upward viasetpriority()requires root privileges or theCAP_SYS_NICELinux capability. Container runtimes (Docker, Kubernetes) strip this capability by default. Consequently, the JVM silently ignores Java thread priorities, granting equal scheduling quotas across all threads.
Thread Context ClassLoader (TCCL)
Every Thread object maintains a reference to a ClassLoader known as its Context ClassLoader:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(customClassLoader);
Why it matters to a Senior Engineer:
Java enforces a strict hierarchical class-loading delegation model (Bootstrap $\to$ Platform $\to$ Application). Core JDK classes (such as java.sql.DriverManager), loaded by the Bootstrap ClassLoader, cannot see third-party driver classes (such as org.postgresql.Driver) residing lower in the hierarchy within the Application ClassLoader.
To resolve this architectural limitation in Service Provider Interfaces (SPI), DriverManager queries the calling thread’s context class loader (Thread.currentThread().getContextClassLoader()), dynamically discovering and loading external vendor drivers at runtime.
🎯 Interview Cheat Sheet
1. 30-Second Elevator Pitch
Runnable is a functional interface representing an abstract task (“what to do”), while Thread is a concrete class managing an execution carrier (“who executes it”). Modern enterprise Java follows “composition over inheritance”: always define tasks via Runnable (or Callable) and delegate execution to thread pools (ExecutorService) or virtual threads (Thread.ofVirtual()), avoiding subclassing Thread. Invoking start() initiates an OS native thread via JNI, whereas calling run() executes the code synchronously on the caller’s stack.
2. 4 Tricky Questions with Deep Answers
Question 1: What occurs at the JVM and OS levels if you accidentally call thread.run() instead of thread.start()?
Answer:
No new operating system thread is spawned. The run() method executes as a standard synchronous method invocation directly on the caller thread’s stack (e.g., main). No kernel system calls (pthread_create / CreateThread) occur, no native memory stack is allocated, and the Thread instance remains in the NEW state. This common mistake blocks the calling thread instead of achieving parallel execution.
Question 2: Why does calling start() a second time on a Thread throw IllegalThreadStateException, even after the thread has reached the TERMINATED state?
Answer:
The Java Thread lifecycle is an irreversible finite state machine. At the entry of start(), HotSpot checks the private field threadStatus:
if (threadStatus != 0) throw new IllegalThreadStateException();
When a thread starts, threadStatus changes, and upon exiting run(), it reaches TERMINATED. The underlying OS kernel thread has already terminated and released its native memory stack (-Xss). Attempting to restart an already dead native thread handle would corrupt JVM HotSpot and JNI runtime structures.
Question 3: In what exact sequence does the JVM search for an UncaughtExceptionHandler when a thread crashes?
Answer: The JVM evaluates exception handlers in a four-tier cascading order:
- Thread-specific handler:
thread.getUncaughtExceptionHandler(); - If absent, the enclosing
ThreadGrouphandler (which recursively delegates up to its parent group); - If absent, the JVM-wide default handler:
Thread.getDefaultUncaughtExceptionHandler(); - If no handlers are defined, the JVM prints the thread name, exception type, and stack trace directly to
System.err.
Question 4: What is the architectural difference between BLOCKED and WAITING states in a Java thread dump?
Answer:
- A thread enters
BLOCKEDexclusively when waiting to acquire a monitor lock for asynchronizedblock or method held by another thread. - A thread enters
WAITINGvoluntarily via explicit wait mechanisms with no timeout:Object.wait(),Thread.join(), orLockSupport.park(). All modern synchronization utilities injava.util.concurrent(such asReentrantLock,CountDownLatch, and Virtual Thread continuation yields) enterWAITING(orTIMED_WAITING), neverBLOCKED.
3. Common Pitfalls and Red Flags
❌ “Extending Thread to implement background tasks.”
Correction: This couples task logic to thread lifecycles and wastes class inheritance. Implement Runnable or Callable and submit tasks to an ExecutorService.
❌ “Using thread.stop() or thread.suspend() to cancel running threads.”
Correction: These methods are deprecated for removal and inherently unsafe: they release all monitor locks immediately, leaving shared objects in corrupted, inconsistent states. Use cooperative cancellation via thread.interrupt().
❌ “Relying on thread.setPriority() to ensure high-priority tasks execute first.”
Correction: Modern Linux server kernels ignore Java priorities for non-root processes. To prioritize work, use priority task queues like PriorityBlockingQueue.