Threads

A thread is an independent path of execution within a process — Java's Thread class and Runnable interface are the foundation for running code concurrently on the JVM.

A process is an independently running program with its own memory space; a thread is a single sequential path of execution within a process, and a process can have many threads that share that process's memory (including the heap) while each maintains its own stack and program counter. Java has supported multithreading since its earliest versions via the Thread class and Runnable interface, giving programs a way to do multiple things concurrently within one JVM instance.

Process vs Thread

A process is an OS-level unit of execution with its own isolated address space; processes generally cannot directly access each other's memory. A thread is a lighter-weight unit of execution living inside a process; all threads within the same process share that process's heap and static memory, but each thread has its own stack, program counter, and set of local variables. Every Java program runs as at least one process containing at least one thread (the main thread).

Process (JVM instance)
  |- shared heap, shared static fields, shared classes
  |- Thread "main"     (own stack, own program counter)
  |- Thread "worker-1"  (own stack, own program counter)
  |- Thread "worker-2"  (own stack, own program counter)
  (all threads see the SAME heap objects and static fields)

Creating Threads: Extending Thread

One way to create a thread is to subclass Thread and override its run() method with the code you want executed on that thread.

Extending Thread directly ties your class to being a Thread and prevents it from extending anything else (Java has single inheritance). Implementing Runnable, shown next, is generally preferred for that reason.

class Greeter extends Thread {
    @Override
    public void run() {
        System.out.println("Hello from " + Thread.currentThread().getName());
    }
}

Greeter g = new Greeter();
g.start(); // begins running on a new thread

Creating Threads: Implementing Runnable

The more flexible approach: implement Runnable (a functional interface with a single run() method) and pass an instance to a Thread's constructor. This decouples 'the task to run' from 'the mechanism that runs it', and lets your task class still extend something else if needed, or be reused across an executor service instead of a raw Thread.

Runnable task = () -> System.out.println("Hello from " + Thread.currentThread().getName());
Thread t = new Thread(task);
t.start();

// Or with a named class:
class PrintTask implements Runnable {
    public void run() { System.out.println("task running"); }
}
new Thread(new PrintTask()).start();

start() vs run()

Calling start() asks the JVM to create a genuinely new thread of execution and have it call run() on that new thread. Calling run() directly is just an ordinary method call — it executes on the CURRENT thread, with no concurrency at all. Confusing the two is one of the most common threading mistakes.

Calling start() more than once on the same Thread instance throws IllegalThreadStateException - a Thread object cannot be restarted after it has completed or while it is running.

Thread t = new Thread(() -> System.out.println("running on: " + Thread.currentThread().getName()));

t.run();   // executes synchronously on the CURRENT thread - no new thread created
Thread t2 = new Thread(() -> System.out.println("running on: " + Thread.currentThread().getName()));
t2.start(); // actually starts a new thread, whose name will differ from the caller's

Thread Lifecycle Concepts

A Java thread moves conceptually through states exposed by Thread.State: NEW (created but not yet started), RUNNABLE (executing, or eligible to be scheduled by the OS/JVM), BLOCKED (waiting to acquire a lock), WAITING / TIMED_WAITING (waiting indefinitely, or for a bounded time, for another thread's action — e.g. Object.wait(), Thread.join()), and TERMINATED (run() has completed, normally or via an uncaught exception).

RUNNABLE in Java's Thread.State does not mean 'currently executing on a CPU core right now' - it means eligible to run; actual CPU scheduling among RUNNABLE threads is an OS-level decision outside Java's control.

NEW --start()--> RUNNABLE <--scheduler swaps in/out--> RUNNABLE
                     |  \
            (needs lock) (waits/sleeps)
                     v      v
                 BLOCKED  WAITING / TIMED_WAITING
                     \      /
                      v    v
                    RUNNABLE (resumes)
                        |
                run() completes
                        v
                   TERMINATED

Common Thread Operations

Thread.sleep(millis) pauses the current thread for at least the given duration (it can be interrupted, and actual wake time can be later, never earlier, than requested). join() blocks the calling thread until the target thread finishes (or a timeout elapses). setName/getName label threads for identification, especially useful in logs and debugging.

Thread worker = new Thread(() -> {
    try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    System.out.println("worker done");
});
worker.setName("worker-1");
worker.start();
worker.join(); // main thread waits here until worker finishes
System.out.println("main continues after worker");

Daemon Threads

A thread is either a user (non-daemon) thread or a daemon thread, set via setDaemon(true) before start(). The JVM keeps running as long as at least one non-daemon thread is alive; once every non-daemon thread finishes, the JVM exits immediately, abruptly terminating any still-running daemon threads without running their remaining code (no finally blocks, no cleanup guarantees). Daemon threads are meant for background housekeeping (e.g. periodic cache eviction) that should never by itself keep the program alive.

Thread daemon = new Thread(() -> {
    while (true) {
        try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
        System.out.println("background housekeeping tick");
    }
});
daemon.setDaemon(true); // must be set before start()
daemon.start();
// If this were the only remaining thread, the JVM would exit and abruptly kill it -
// daemon threads never keep the JVM alive on their own.

Interruption

Java's interruption mechanism is cooperative, not forceful: calling thread.interrupt() sets that thread's interrupt status flag; it does NOT forcibly stop the thread. Blocking methods like Thread.sleep(), Object.wait(), and many java.util.concurrent operations respond to interruption by throwing InterruptedException and clearing the flag — code must catch that and decide how to react (commonly, clean up and return, or re-set the interrupt flag for calling code to observe).

Never silently swallow InterruptedException (an empty catch block) - either handle it meaningfully or, at minimum, call Thread.currentThread().interrupt() to restore the flag so surrounding code can still observe the interruption request.

Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // do work, periodically checking the flag
    }
    System.out.println("worker noticed interruption and is exiting cleanly");
});
worker.start();
// ... later ...
worker.interrupt(); // requests cooperative shutdown - worker must check for it

Practical Usage

Directly creating raw Thread objects is common in learning contexts and simple scripts, but production code generally prefers higher-level abstractions — thread pools via ExecutorService, or (Java 21+) lightweight virtual threads — over managing raw threads by hand, mainly because raw threads are relatively expensive to create in large numbers and offer no built-in task queuing, backpressure, or lifecycle management.

Edge Cases

An uncaught exception thrown inside a thread's run() terminates only that thread — it does not propagate to, or crash, any other thread (including the thread that started it), and by default just prints a stack trace via the thread's uncaught exception handler. This is a common surprise: a background thread silently dying from an exception, with no visible effect on the rest of the program unless you've set an explicit UncaughtExceptionHandler.

Thread t = new Thread(() -> { throw new RuntimeException("oops"); });
t.setUncaughtExceptionHandler((thread, ex) ->
    System.out.println(thread.getName() + " died: " + ex.getMessage()));
t.start();
// main thread continues running normally - the exception did NOT propagate to it

Mental Model

Picture a process as a shared office building (one shared address space, one heap) and each thread as an employee working inside it. All employees can walk to the same shared filing cabinets (the heap, static fields) and read or modify the same documents — which is exactly why concurrent access needs coordination. But each employee also has their own private desk (stack) with their own notes (local variables) that no one else touches directly.

Quick Reference

new Thread(runnable).start();      // creates and starts a new thread
thread.run();                       // WRONG for concurrency - runs on caller's thread
thread.join();                      // wait for thread to finish
thread.setDaemon(true);             // before start() - background thread, doesn't keep JVM alive
thread.interrupt();                 // cooperative request to stop
Thread.currentThread();             // reference to the running thread
Thread.sleep(millis);               // pause current thread, throws InterruptedException

States: NEW -> RUNNABLE <-> BLOCKED/WAITING/TIMED_WAITING -> TERMINATED

Key Takeaways

A process has its own isolated memory; threads within a process share the heap and static fields but each has its own stack and program counter.

Prefer implementing Runnable over extending Thread, since it doesn't consume your class's single inheritance slot and decouples the task from the execution mechanism.

start() creates a genuinely new thread of execution; calling run() directly just executes synchronously on the current thread - no concurrency at all.

A daemon thread never keeps the JVM alive on its own - once all non-daemon threads finish, the JVM exits and abruptly kills remaining daemon threads without cleanup.

interrupt() only sets a cooperative flag; it does not forcibly stop a thread - code must check the flag or handle InterruptedException to respond.

An uncaught exception in one thread's run() terminates only that thread; it does not propagate to or crash other threads.

Thread t = new Thread(() -> doWork()); t.run(); // no new thread is created - runs synchronously on the caller

Thread t = new Thread(() -> doWork()); t.start(); // actually starts a new thread run() is just an ordinary method call; only start() asks the JVM to begin a new thread of execution.

while (!stopped) { doWork(); } catch (InterruptedException e) { } // empty catch, interruption silently swallowed while (!Thread.currentThread().isInterrupted()) { try { doWork(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore the flag break; } }

Silently swallowing InterruptedException hides a shutdown request from the rest of the program - always either handle it meaningfully or restore the interrupt flag.

Thread t = new Thread(worker); t.start(); t.start(); // attempting to reuse/restart the same Thread instance

Thread t1 = new Thread(worker); t1.start(); // later, to run again, create a NEW Thread instance: Thread t2 = new Thread(worker); t2.start();

A Thread instance cannot be restarted after it has been started - calling start() a second time throws IllegalThreadStateException. Create a new Thread for each run.

What is the key difference between a process and a thread?

A process has its own isolated memory space; a thread is a lighter-weight execution unit within a process that shares that process's heap and static fields with other threads in the same process, while maintaining its own stack and program counter.

Why is implementing Runnable generally preferred over extending Thread?

Implementing Runnable doesn't consume the class's single inheritance slot (it can still extend something else), and it separates 'what task to run' from 'the mechanism that runs it', making the task reusable with thread pools or other executors instead of only raw Thread objects.

What actually happens if you call thread.run() instead of thread.start()?

run() is just an ordinary method invocation - it executes synchronously on the calling thread, with no new thread created and no concurrency involved at all.

What happens to a daemon thread when all non-daemon threads in the JVM finish?

The JVM exits immediately once no non-daemon threads remain, abruptly terminating any still-running daemon threads without running their remaining code - no finally blocks execute, no cleanup is guaranteed.

Does calling thread.interrupt() forcibly stop the target thread?

No. It only sets that thread's interrupt status flag (and can cause certain blocking calls like sleep()/wait() to throw InterruptedException). The target thread must cooperatively check the flag or handle the exception to actually respond to the interruption request.

What happens when an exception is thrown and not caught inside a thread's run() method?

That thread terminates; the exception does not propagate to any other thread, including whichever thread created or started it. By default the stack trace is printed via the thread's uncaught exception handler, but the rest of the program continues running unaffected unless it was specifically monitoring that thread.

Can you call start() twice on the same Thread instance to run it again?

No - calling start() a second time on a Thread that has already been started throws IllegalThreadStateException. A new Thread instance must be created for each separate run.

Write code that creates two threads via Runnable, each printing its own thread name, and waits for both to finish using join() before printing 'all done'.

Runnable task = () -> System.out.println("running on: " + Thread.currentThread().getName()); Thread t1 = new Thread(task, "worker-1"); Thread t2 = new Thread(task, "worker-2"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("all done");

t1 and t2 run concurrently (each printing its own name, in unpredictable relative order), and the main thread blocks on both join() calls until each finishes, guaranteeing 'all done' prints only after both workers have completed.

Does this create a new thread? What gets printed for 'thread=' in the run() output?

No new thread is created, because run() was called directly rather than start(). The 'in run' line prints the CURRENT (calling/main) thread's name - typically "main" - since run() is just an ordinary synchronous method call in this case.

Write a thread that sets an UncaughtExceptionHandler and then throws an uncaught RuntimeException inside run(), showing that the main thread is unaffected.

Thread t = new Thread(() -> { throw new RuntimeException("worker failed"); }); t.setUncaughtExceptionHandler((thread, ex) -> System.out.println(thread.getName() + " crashed: " + ex.getMessage())); t.start(); try { t.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("main thread is still running fine");

The handler prints something like 'Thread-0 crashed: worker failed', and 'main thread is still running fine' still prints afterward - the uncaught exception terminated only the worker thread, never propagating to or crashing main.

Does this program hang forever because of the infinite loop in the daemon thread?

No. Because daemon was marked as a daemon thread before start(), it does not keep the JVM alive. Once main finishes (the only non-daemon thread completes), the JVM exits immediately, abruptly terminating the daemon thread's infinite loop along with it - the program does not hang.

class Worker extends Thread {
    public void run() { System.out.println("in run, thread=" + Thread.currentThread().getName()); }
}
Worker w = new Worker();
w.run();
System.out.println("main thread=" + Thread.currentThread().getName());
Thread daemon = new Thread(() -> {
    while (true) { }
});
daemon.setDaemon(true);
daemon.start();
System.out.println("main finishing");
// program ends here (main returns)