Creating Threads
Extending Thread vs implementing Runnable, lambdas, start() vs run(), naming, daemon threads, priorities, uncaught-exception handlers, and why production code uses executors instead.
#Introduction
Java gives you two classic ways to define work for a thread — extend Thread or implement Runnable — plus Callable for tasks that return a value (covered with executors). This chapter covers both, the critical start() vs run() difference, and the thread properties every backend developer should know: names, daemon status, priorities and uncaught-exception handlers.
#Way 1 — Extend Thread
class EmailSender extends Thread {
@Override
public void run() {
System.out.println("Sending email on " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
EmailSender sender = new EmailSender();
sender.start();
System.out.println("Main continues on " + Thread.currentThread().getName());
}
}Main continues on main
Sending email on Thread-0The order of the two lines is not guaranteed — the threads run independently and the scheduler decides.
#Way 2 — Implement Runnable
class EmailTask implements Runnable {
@Override
public void run() {
System.out.println("Sending email on " + Thread.currentThread().getName());
}
}
Thread thread = new Thread(new EmailTask());
thread.start();Runnable is a functional interface (void run()), so a lambda works too:
Thread thread = new Thread(() -> System.out.println("Sending email"));
thread.start();Java 21 Builders
Thread t1 = Thread.ofPlatform().name("email-sender").start(() -> sendEmail());
Thread t2 = Thread.ofVirtual().start(() -> sendEmail()); // a virtual thread#Thread vs Runnable — Which to Use?
| Extend Thread | Implement Runnable |
|---|---|
| Uses up your only superclass (Java has single inheritance) | Class can still extend something else |
| Mixes the task with the thread mechanism | Separates the task (what) from the thread (how) |
| The task can't be submitted to an executor as-is | Can be passed to Thread, ExecutorService, CompletableFuture |
| Rarely the right design | Preferred |
#start() vs run() — the Classic Trap
Thread thread = new Thread(() ->
System.out.println("Running on " + Thread.currentThread().getName()));
thread.run(); // Running on main — just a normal method call!
thread.start(); // Running on Thread-0 — a new threadstart() | run() |
|---|---|
Asks the JVM to create a new thread, which then calls run() | Executes run() on the current thread, like any method |
| Returns immediately — work happens concurrently | Blocks until run() finishes |
Can be called only once per Thread object | Can be called any number of times |
Q1Calling start() Twice
Thread thread = new Thread(() -> System.out.println("Hello"));
thread.start();
thread.start(); // throwsException in thread "main" java.lang.IllegalThreadStateExceptionA thread can't be restarted once it's started — create a new Thread (or, better, reuse a thread pool).
#Naming Threads
Default names like Thread-0 make logs and thread dumps useless. Name your threads:
Thread worker = new Thread(task, "invoice-generator");
worker.setName("invoice-generator-1");In production, name pool threads with a ThreadFactory (covered with ThreadPoolExecutor) — e.g. order-processor-1, order-processor-2.
#Daemon Threads
A daemon thread is a background thread that doesn't keep the JVM alive. When only daemon threads remain, the JVM exits — abruptly stopping them.
Thread heartbeat = new Thread(() -> {
while (true) {
System.out.println("heartbeat");
sleepQuietly(1000);
}
});
heartbeat.setDaemon(true); // must be set BEFORE start()
heartbeat.start();| User (non-daemon) thread | Daemon thread |
|---|---|
| JVM waits for it to finish | JVM doesn't wait — kills it on exit |
Default for threads created by main | Inherited from a daemon parent, or set explicitly |
| Business work | Background support: GC, monitoring, cache cleanup |
#Thread Priorities
thread.setPriority(Thread.MAX_PRIORITY); // 10; MIN_PRIORITY = 1, NORM_PRIORITY = 5Priorities are only a hint to the OS scheduler — many platforms largely ignore them. Never rely on priorities for correctness or ordering.
#Exceptions in Threads
An exception thrown in another thread does not propagate to the thread that started it:
try {
new Thread(() -> { throw new IllegalStateException("boom"); }).start();
} catch (Exception e) {
System.out.println("Never printed"); // the exception happens on another thread
}Exception in thread "Thread-0" java.lang.IllegalStateException: boomThe uncaught exception terminates only that thread and is printed to System.err. Install a handler to log it properly:
Thread worker = new Thread(task, "report-worker");
worker.setUncaughtExceptionHandler((t, e) ->
log.error("Thread {} failed", t.getName(), e));
worker.start();
// Or a JVM-wide default
Thread.setDefaultUncaughtExceptionHandler((t, e) -> log.error("Uncaught in {}", t.getName(), e));#Runnable vs Callable — a Preview
| Runnable | Callable<V> |
|---|---|
void run() | V call() throws Exception |
| No result | Returns a result |
| Can't throw checked exceptions | Can throw checked exceptions |
Accepted by Thread and executors | Accepted by executors (not by Thread directly) |
Callable and Future are covered in the Executor Framework chapter.
#Why Not Create Threads Manually in Production?
// Anti-pattern: one new thread per request
@PostMapping("/orders")
public void create(@RequestBody Order order) {
new Thread(() -> process(order)).start();
}- Under load this creates unbounded threads and can exhaust memory (
OutOfMemoryError: unable to create native thread). - Thread creation cost is paid on every request.
- There's no way to limit, queue, monitor or shut down the work.
- Exceptions disappear into
System.err.
Use an ExecutorService (or Spring's @Async with a configured pool) instead — or, on Java 21, virtual threads.
#Interview Questions
Q1What are the ways to create a thread?
Extend Thread and override run(), or implement Runnable (or Callable) and pass it to a Thread or an executor. Java 21 adds Thread.ofPlatform() / Thread.ofVirtual() builders. In practice, tasks are submitted to an ExecutorService.
Q2Why is implementing Runnable preferred over extending Thread?
It keeps your single superclass free, separates the task from the threading mechanism, and works with executors and CompletableFuture.
Q3What happens if you call run() instead of start()?
run() executes on the current thread like an ordinary method — no new thread is created.
Q4Can you start a thread twice?
No — the second start() throws IllegalThreadStateException.
Q5What is a daemon thread?
A background thread that doesn't prevent the JVM from exiting. The JVM kills daemon threads when all user threads have finished; setDaemon(true) must be called before start().
Q6How do you handle exceptions thrown inside a thread?
Catch them inside run(), or set an UncaughtExceptionHandler on the thread (or globally). With executors, use Future.get() to see exceptions from submit().
#Common Mistakes
- Calling
run()instead ofstart()— nothing runs concurrently. - Starting the same
Threadobject twice. - Leaving threads unnamed — thread dumps full of
Thread-37are impossible to debug. - Using daemon threads for important work — they're killed without running
finally. - Relying on priorities or on output order — both are up to the scheduler.
- Creating a new thread per request in a server.
#Best Practices
- Implement
Runnable/Callableand submit tasks to an executor. - Give every thread a meaningful name.
- Catch exceptions inside tasks or install an
UncaughtExceptionHandlerthat logs them. - Use daemon threads only for disposable background work.
- Never assume any ordering between threads unless you enforce it.
#Key Takeaways
- Define work with
Runnable(orCallable), not by extendingThread. start()creates a new thread;run()is a plain method call.- A thread can be started only once.
- Daemon threads don't keep the JVM alive and can die mid-work.
- Exceptions in a thread don't reach the caller — handle or log them explicitly.
- Production code uses executors or virtual threads instead of manual
new Thread().