Memra

Threads: Runnable, Thread, and executors

◈ 5 cards

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
t1t2t3t4mainnew Threadt.start()next stmtnext stmtThread-0run()hello from ATwo call stacks. main does not wait for run().
start() asks the JVM for a second call stack: run() executes on Thread-0 while main carries on without waiting for it.
t1t2t3t4mainnew Threadt.run()hello from Anext stmtThread-0No concurrency: Thread-0 never starts.
The exam trap. t.run() is an ordinary method call, so the work happens on main and no second thread is ever created — the Thread-0 row never fills in.
NORMAL ~/memra/learn/comp-308/threads-runnable-executors utf-8 LF