☕ Java

Creating Threads

Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.

Extending Thread — Direct Subclassing

The Thread class itself implements Runnable, and its run() method is the entry point for the thread's execution. Subclassing Thread and overriding run() is the most direct way to define a thread's behavior: the task definition and the execution mechanism are the same object. After constructing an instance of the subclass, call start() to launch the OS thread; calling run() directly executes the method on the calling thread without any threading. The primary limitation of Thread subclassing is coupling: the task cannot be separated from the thread. The same logic cannot be submitted to an ExecutorService, scheduled with a ScheduledExecutorService, or composed with CompletableFuture without wrapping. If the task logic needs to be reused across different execution contexts — run directly in tests, submitted to a thread pool in production, wrapped in a retry framework — subclassing Thread makes that reuse harder. The class also cannot extend any other class, which is relevant if the task logic naturally belongs to a domain class. Thread subclassing is appropriate in a narrow set of cases: when you need to override Thread behavior beyond run() (such as providing a custom ThreadGroup, setting the stack size via the Thread constructor, or overriding Thread.interrupt()); when writing educational examples that make the thread model explicit; or when the task is genuinely inseparable from the threading mechanism (unusual in practice). In all other cases, Runnable or Callable is the better choice. The Thread class provides several constructors that allow setting the name, thread group, daemon status, and stack size at construction time. These constructors accept a Runnable argument, which brings subclassing and Runnable together: a Thread subclass that supplies no Runnable to super() and overrides run() is pure subclassing; a Thread constructed with a Runnable argument delegates run() to that Runnable. The latter is the mechanism used by thread pool implementations internally but is not typically used directly in application code.
Java
// ── Extending Thread — the oldest approach ───────────────────────────
class CountingThread extends Thread {
    private final int from;
    private final int to;
    private int result;

    CountingThread(int from, int to) {
        super("counter-" + from + "-to-" + to);  // thread name in constructor
        this.from = from;
        this.to   = to;
    }

    @Override
    public void run() {
        // This method runs on the new thread
        int sum = 0;
        for (int i = from; i <= to; i++) {
            sum += i;
            if (Thread.currentThread().isInterrupted()) {
                System.out.println(getName() + " interrupted at i=" + i);
                return;   // cooperative cancellation — exit cleanly
            }
        }
        this.result = sum;
        System.out.printf("[%s] sum(%d..%d) = %d%n", getName(), from, to, sum);
    }

    // Custom accessor — only valid after join() returns:
    int getResult() { return result; }
}

// ── Creating, starting, and joining ───────────────────────────────────
CountingThread t1 = new CountingThread(1, 1_000_000);
CountingThread t2 = new CountingThread(1_000_001, 2_000_000);

t1.start();   // launches OS thread; run() executes concurrently
t2.start();

// Calling run() directly — WRONG, runs on calling thread, no new thread:
// t1.run();   // does NOT start a new thread; runs synchronously

t1.join();    // wait for t1 to finish
t2.join();    // wait for t2 to finish

// Happens-before established by join(): result is safely readable:
System.out.println("Total: " + (t1.getResult() + t2.getResult()));

// ── Illegal restart ───────────────────────────────────────────────────
try {
    t1.start();   // IllegalThreadStateException — thread is TERMINATED
} catch (IllegalThreadStateException e) {
    System.out.println("Cannot restart: " + e.getMessage());
}

// ── Constructor overloads — name, group, stack size ───────────────────
ThreadGroup group = new ThreadGroup("io-threads");
Thread withGroup = new Thread(group, () -> {}, "io-reader-1");
Thread withStack = new Thread(null, () -> {}, "deep-recursion", 4 * 1024 * 1024); // 4MB stack

// ── When to use Thread subclassing (rare) ────────────────────────────
// - Need to override interrupt() for custom cancellation logic
// - Writing a thread pool and need fine-grained Thread control
// - Educational code where making threading explicit is the point
// In all other cases, prefer Runnable or Callable:
// new Thread(() -> { /* task */ }, "name").start();   // Runnable lambda — simpler

Implementing Runnable — Decoupling Task from Thread

Runnable is a functional interface with a single abstract method void run(). It represents a task that performs some action, produces no return value, and declares no checked exceptions. Runnable is the correct abstraction when you want to define a task independently of how or where it will execute. The same Runnable instance can be wrapped in a Thread, submitted to an ExecutorService, passed to CompletableFuture.runAsync(), scheduled with a ScheduledExecutorService, or executed directly in a test — all without changing the Runnable itself. Because Runnable is a functional interface, lambdas and method references that match the void () signature implement it automatically. This makes Runnable the natural representation for fire-and-forget concurrent tasks in modern Java. () -> System.out.println("hello") is a Runnable. SomeClass::staticVoidMethod is a Runnable. A lambda that captures local variables creates a Runnable whose run() method has access to those captured values. The limitation of Runnable is its void return type and absence of declared checked exceptions. A Runnable cannot return a computed result to its caller; any output must be communicated through shared state (a field, a queue, an AtomicReference), which reintroduces synchronization concerns. A Runnable cannot propagate checked exceptions from run(); any checked exception must be caught and handled within run(), logged, stored in a field, or wrapped in an unchecked exception. For tasks that need to return a result or propagate checked exceptions, Callable is the correct choice. The transition from Thread subclassing to Runnable-based threading is the step from coupling to decoupling. In practice, most concurrent application code should never call new Thread(...).start() directly at all — it should submit Runnables (or Callables) to an ExecutorService, which manages thread lifecycle, reuse, and error handling. But understanding Runnable with raw threads is the prerequisite for understanding what executors do under the hood.
Java
// ── Runnable as a class — explicit implementation ────────────────────
class DataLoader implements Runnable {
    private final String url;
    private volatile String result;   // volatile for cross-thread visibility

    DataLoader(String url) { this.url = url; }

    @Override
    public void run() {
        // Simulate loading data:
        System.out.printf("[%s] Loading from %s%n", Thread.currentThread().getName(), url);
        try {
            Thread.sleep(300);  // simulate I/O
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();  // restore flag
            return;
        }
        this.result = "DATA_FROM_" + url.toUpperCase();
        System.out.printf("[%s] Done loading from %s%n", Thread.currentThread().getName(), url);
    }

    String getResult() { return result; }  // safe after join()
}

DataLoader loader = new DataLoader("https://api.example.com/data");
Thread loaderThread = new Thread(loader, "data-loader-1");
loaderThread.start();
loaderThread.join();
System.out.println("Result: " + loader.getResult());  // safe after join()

// ── Runnable as a lambda — the modern idiom ───────────────────────────
Runnable printHello = () -> System.out.println("Hello from " + Thread.currentThread().getName());
new Thread(printHello, "greeter").start();

// Capturing local variables (must be effectively final):
String prefix = "TASK";
int taskId    = 42;
Runnable captureExample = () ->
    System.out.printf("[%s-%d] Running on %s%n", prefix, taskId, Thread.currentThread().getName());
new Thread(captureExample, "task-42").start();

// ── Same Runnable, multiple execution contexts ────────────────────────
Runnable task = () -> {
    System.out.println("Executing on: " + Thread.currentThread().getName());
};

// As a Thread:
new Thread(task, "direct-thread").start();

// In an ExecutorService (thread pool):
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.execute(task);   // execute() accepts Runnable
pool.shutdown();

// Synchronously in tests or single-threaded contexts:
task.run();   // no new thread — runs on calling thread

// ── Runnable cannot return a result or throw checked exceptions ───────
// Wrong: wrapping result in shared state adds complexity
class ResultCapture implements Runnable {
    volatile Object result;
    volatile Exception error;

    @Override
    public void run() {
        try {
            result = computeSomething();  // result stored in field
        } catch (Exception e) {
            error = e;                    // error stored in field — awkward
        }
    }
}
// This is exactly what Callable + Future solves properly.

// ── Runnable composition ──────────────────────────────────────────────
// Runnables compose via sequential execution:
Runnable step1 = () -> System.out.println("Step 1");
Runnable step2 = () -> System.out.println("Step 2");
Runnable composed = () -> { step1.run(); step2.run(); };

new Thread(composed, "sequential-task").start();

Implementing Callable — Return Values, Exceptions, and Future

Callable<V> is a functional interface with a single abstract method V call() throws Exception. It is the correct abstraction for tasks that need to return a computed result or propagate a checked exception to the caller. Where Runnable's run() is void and declares no checked exceptions, Callable's call() returns V and declares throws Exception, covering both gaps. The Callable interface was introduced in Java 5 alongside the java.util.concurrent package specifically to address Runnable's limitations. Callable cannot be submitted to a raw Thread — Thread's constructor only accepts Runnable. To execute a Callable on a background thread and retrieve its result, use ExecutorService.submit(Callable<T>), which returns a Future<T>. The Future represents the pending result: Future.get() blocks the calling thread until the Callable's call() method completes and returns the result, or until an exception is thrown. If call() threw a checked exception, Future.get() wraps it in an ExecutionException whose cause is the original exception. If the task was cancelled, get() throws CancellationException. If the calling thread is interrupted while waiting in get(), InterruptedException is thrown. FutureTask<V> is the concrete implementation that bridges Callable and Thread. It implements both Runnable and Future<V>: it wraps a Callable, can be passed to a Thread as a Runnable, and exposes Future semantics for retrieving the result. FutureTask is the mechanism that ExecutorService.submit(Callable) uses internally. Using FutureTask directly is appropriate when you need to run a Callable on a thread you manage yourself, or when you need the cancellation semantics of Future without an ExecutorService. The combination of Callable, Future, and ExecutorService is the workhorse of structured concurrent computation in Java before Project Loom. Multiple Callables can be submitted to a thread pool, their Futures collected, and results retrieved as they complete via Future.get() or ExecutorService.invokeAll() (which waits for all) or ExecutorService.invokeAny() (which returns the first successful result). This pattern underlies parallel data processing, parallel I/O, and any workflow that fans out into concurrent subtasks and then joins the results.
Java
// ── Callable — returns a result and can throw ────────────────────────
import java.util.concurrent.*;

Callable<Integer> sumTask = () -> {
    System.out.println("Computing sum on: " + Thread.currentThread().getName());
    Thread.sleep(300);  // simulate work — checked InterruptedException propagates naturally
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;  // 5050
};

// ── Submitting to ExecutorService — the standard approach ─────────────
ExecutorService executor = Executors.newFixedThreadPool(4);

Future<Integer> future = executor.submit(sumTask);
System.out.println("Task submitted, continuing on main thread...");

// Future.get() blocks until the task completes:
try {
    Integer result = future.get();        // blocks here
    System.out.println("Sum = " + result);  // 5050
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();    // restore flag
} catch (ExecutionException e) {
    System.err.println("Task threw: " + e.getCause());  // unwrap original exception
}

// ── Callable that throws a checked exception ──────────────────────────
Callable<String> riskyTask = () -> {
    if (Math.random() < 0.5) throw new IOException("Network failed");
    return "Success";
};

Future<String> riskyFuture = executor.submit(riskyTask);
try {
    String res = riskyFuture.get();
    System.out.println("Result: " + res);
} catch (ExecutionException e) {
    Throwable cause = e.getCause();  // the original IOException
    System.err.println("Task failed: " + cause.getMessage());
}

// ── Multiple Callables — invokeAll ────────────────────────────────────
List<Callable<Integer>> tasks = List.of(
    () -> { Thread.sleep(100); return 1; },
    () -> { Thread.sleep(200); return 2; },
    () -> { Thread.sleep(150); return 3; }
);

// invokeAll() submits all, waits for all to complete:
List<Future<Integer>> futures = executor.invokeAll(tasks);
int total = 0;
for (Future<Integer> f : futures) {
    total += f.get();   // all are already done — get() returns immediately
}
System.out.println("Total: " + total);  // 6

// ── invokeAny — first successful result wins ──────────────────────────
List<Callable<String>> redundant = List.of(
    () -> { Thread.sleep(500); return "slow-result"; },
    () -> { Thread.sleep(100); return "fast-result"; },
    () -> { Thread.sleep(300); return "medium-result"; }
);
String first = executor.invokeAny(redundant);  // returns "fast-result"
System.out.println("First: " + first);          // fast-result

// ── FutureTask — Callable in a Thread without ExecutorService ─────────
Callable<Double> piEstimate = () -> {
    long hits = 0, total = 1_000_000;
    for (long i = 0; i < total; i++) {
        double x = Math.random(), y = Math.random();
        if (x * x + y * y <= 1.0) hits++;
    }
    return 4.0 * hits / total;
};

FutureTask<Double> futureTask = new FutureTask<>(piEstimate);
Thread piThread = new Thread(futureTask, "pi-estimator");
piThread.start();

// FutureTask implements Future<Double>:
System.out.println("π ≈ " + futureTask.get());  // blocks until done

// ── Future timeout and cancellation ──────────────────────────────────
Callable<String> longRunning = () -> { Thread.sleep(10_000); return "done"; };
Future<String> slowFuture = executor.submit(longRunning);

try {
    String r = slowFuture.get(1, TimeUnit.SECONDS);  // wait at most 1 second
} catch (TimeoutException e) {
    System.out.println("Too slow — cancelling");
    slowFuture.cancel(true);    // true = interrupt the running thread
    System.out.println("Cancelled: " + slowFuture.isCancelled());  // true
}

executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.
Thread Priority
Thread priority in Java is an integer hint to the OS scheduler indicating the relative importance of a thread compared to others. Java defines a scale from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10) with Thread.NORM_PRIORITY (5) as the default, and every thread inherits the priority of the thread that created it. The critical word in this definition is hint: thread priority is advisory, not mandatory. The JVM maps Java priorities to native OS thread priorities, and the OS scheduler uses those priorities according to its own scheduling policy, which varies by operating system, scheduler configuration, and system load. On some platforms, priority has a measurable effect on scheduling frequency; on others, it is almost entirely ignored. Priority must never be used as a correctness mechanism — any program that requires a thread to run before another for correctness, rather than merely preferring it, is broken and will fail on any platform where priorities are not honored. This entry covers the priority scale, inheritance, platform mapping, the correctness prohibition, starvation, priority inversion, and the narrow set of cases where priority hints are legitimately useful.