Threads: Runnable, Thread, and executors
Defining a task with Runnable, launching it with start() (not run()), and managing pools with ExecutorService.
A task and the thread that runs it
Java splits the *what* from the *who*: a task is a block of work (a Runnable), and a thread is the worker that executes it. Keeping them separate is the cleaner design, and it is exactly what the Greenhouse project does in Part 2 when each Event becomes a Runnable and gets its own thread.
Runnable is a functional interface with one method:
public interface Runnable {
void run();
}
The classic pattern is to implement Runnable, then hand the task to a Thread:
class Greeter implements Runnable {
private final String name;
Greeter(String name) { this.name = name; }
public void run() {
System.out.println("hello from " + name);
}
}
Thread t = new Thread(new Greeter("A"));
t.start(); // schedules run() on a NEW thread
The single most important rule: call start(), *not* run(). start() asks the JVM to create a new call stack and invoke run() on it concurrently. Calling run() directly just executes the method on the current thread — no concurrency happens at all, and it is a favourite exam trap.
Extending Thread vs implementing Runnable
You *can* subclass Thread and override run(), but prefer implementing Runnable: a class can implement many interfaces but extend only one class, so subclassing Thread burns your single inheritance slot. Runnable also lets the same task run on a raw thread *or* an executor.
Executors: don't hand-manage threads
Creating one Thread per task does not scale — thread creation is expensive and unbounded thread counts crush the machine. The java.util.concurrent executor framework manages a reusable pool for you. You submit tasks; the pool's threads pick them up.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
final int id = i;
pool.execute(() -> System.out.println("task " + id));
}
pool.shutdown(); // no new tasks; finishes the queued ones, then dies
Executors.newFixedThreadPool(n) keeps n threads alive and queues extra work. newCachedThreadPool() grows and reuses threads on demand. newSingleThreadExecutor() runs tasks one at a time in submission order. Always shutdown() an executor — its threads are non-daemon by default, so a live pool keeps the JVM from exiting.
Callable and Future when you need a result
Runnable.run() returns nothing and cannot throw checked exceptions. When a task must return a value, use Callable<V> and submit(), which hands back a Future<V> whose get() blocks until the result is ready:
Future<Integer> f = pool.submit(() -> 6 * 7);
int answer = f.get(); // blocks; answer == 42