DevNotes
Support
Multithreading · Chapter 02

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.

◷ 7 min read§ 15 sections? 7 interview questions{ } 12 code examples

#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

Java
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());
    }
}
One possible output
Main continues on main
Sending email on Thread-0

The order of the two lines is not guaranteed — the threads run independently and the scheduler decides.

#Way 2 — Implement Runnable

Java
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:

Java
Thread thread = new Thread(() -> System.out.println("Sending email"));
thread.start();

Java 21 Builders

Java
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 ThreadImplement Runnable
Uses up your only superclass (Java has single inheritance)Class can still extend something else
Mixes the task with the thread mechanismSeparates the task (what) from the thread (how)
The task can't be submitted to an executor as-isCan be passed to Thread, ExecutorService, CompletableFuture
Rarely the right designPreferred

#start() vs run() — the Classic Trap

Java
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 thread
start()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 concurrentlyBlocks until run() finishes
Can be called only once per Thread objectCan be called any number of times
thread.start()
JVM creates a new OS thread
New thread calls run()
start() has already returned to the caller
Q1Calling start() Twice
Java
Thread thread = new Thread(() -> System.out.println("Hello"));
thread.start();
thread.start();   // throws
Exception
Exception in thread "main" java.lang.IllegalThreadStateException

A 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:

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

Java
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) threadDaemon thread
JVM waits for it to finishJVM doesn't wait — kills it on exit
Default for threads created by mainInherited from a daemon parent, or set explicitly
Business workBackground support: GC, monitoring, cache cleanup

#Thread Priorities

Java
thread.setPriority(Thread.MAX_PRIORITY);   // 10; MIN_PRIORITY = 1, NORM_PRIORITY = 5

Priorities 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:

Java
try {
    new Thread(() -> { throw new IllegalStateException("boom"); }).start();
} catch (Exception e) {
    System.out.println("Never printed");   // the exception happens on another thread
}
Output
Exception in thread "Thread-0" java.lang.IllegalStateException: boom

The uncaught exception terminates only that thread and is printed to System.err. Install a handler to log it properly:

Java
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

RunnableCallable<V>
void run()V call() throws Exception
No resultReturns a result
Can't throw checked exceptionsCan throw checked exceptions
Accepted by Thread and executorsAccepted by executors (not by Thread directly)

Callable and Future are covered in the Executor Framework chapter.

#Why Not Create Threads Manually in Production?

Java
// 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 of start() — nothing runs concurrently.
  • Starting the same Thread object twice.
  • Leaving threads unnamed — thread dumps full of Thread-37 are 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 / Callable and submit tasks to an executor.
  • Give every thread a meaningful name.
  • Catch exceptions inside tasks or install an UncaughtExceptionHandler that 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 (or Callable), not by extending Thread.
  • 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().