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
// ── 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 — simplerImplementing Runnable — Decoupling Task from Thread
// ── 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 — 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);