Concurrency · Session 7

Thread Pools, ExecutorService, Callable & Future

Everything I need to run tasks asynchronously in Java without manually juggling threads — pooling, submitting work, getting results back, and choosing the right tool for the job.

Thread Pool Executor framework ExecutorService Pool types Runnable Callable Future submit / invokeAll / invokeAny shutdown
+ = my own enrichment / correction (not from source)
the loop that takes the server down

A request comes in, you do the obvious thing: new Thread(task).start(). It works. Then traffic spikes — 10,000 requests — and you’ve just asked the OS for 10,000 threads.

Each one carries roughly a megabyte of stack and a slice of the scheduler’s attention. The machine spends more time switching between threads than running them, memory balloons, and the process falls over. The cost wasn’t the work — it was creating and destroying a thread for every task.

A thread pool hires a small fixed crew once and feeds them tasks from a queue. This note is how you run work that way in Java: the Executor framework, Runnable vs Callable, the Future you collect results from, and the shutdown you must not forget.

1 THREAD / TASK 10k tasks 💥 10,000 OS threads · gigabytes of stack · scheduler thrash → crash THREAD POOL 10k tasks … queue pool · 4 workers (reused)W1W2W3W4 ✓ 4 threads, reused — bounded memory, no create/destroy churn
I

Detailed Study Notes

Built from scratch. Each concept: claim → plain explanation → why it matters → traps. + marks what I added beyond the core material.

1The problem thread pools solve

Spinning up a brand-new thread for every task is the concurrency equivalent of hiring and firing a cook for each order.

Picture an online food-ordering system. Every order has to be processed, sent to the kitchen, and have its status pushed back to the customer. The naive approach is: a new order arrives, so I create a fresh worker (thread) to handle it, and when it's done that worker disappears. This breaks down fast:

The fix is a team of trained workers on standby. They're already there, ready to grab the next order the instant it lands. In software terms that standby crew is a thread pool: a fixed, reusable set of threads that pick tasks off a queue.

Analogy

A restaurant doesn't hire a new chef per plate. It keeps a kitchen brigade. Orders queue on the rail, free cooks grab the next ticket. The pool is that brigade.

Why it matters

Thread creation costs real time and memory (stack, OS scheduling structures). At scale, reuse is the entire game — it converts an unbounded, spiky cost into a bounded, predictable one.

Pitfall

+Unbounded thread creation is a classic outage cause: each Java thread reserves ~512KB–1MB of stack by default, so tens of thousands of threads can exhaust memory and crash the JVM long before the CPU is the bottleneck.

2Thread pool in Java + the Executor framework

client submit() task queue pool · fixed workersworker 1worker 2worker 3worker 4 Future the receipt get() — client blocks here only when it actually needs the value
You hand tasks to the pool and get a Future back — the workers are reused, never recreated per task
A thread pool maintains a set of ready-to-use threads; in Java you almost never build one by hand — you ask the Executors factory for one.

A thread pool is a well-organized team of workers that avoids constant creation/destruction and efficiently handles a stream of tasks. In Java this lives in the Executor framework (package java.util.concurrent, since Java 5). Two pieces matter:

+Layered interfaces

The interfaces nest: Executor (just execute(Runnable)) ⊂ ExecutorService (adds submit, shutdown, invokeAll/Any) ⊂ ScheduledExecutorService (adds delayed/periodic scheduling). You program to the interface, the factory picks the implementation.

Why it matters

The framework cleanly separates what work to do (the task you submit) from how/when it runs (the pool's policy). You write tasks; the executor owns threading.

3Types of thread pools

Four convenience pools cover most needs; reach for ThreadPoolExecutor when none fit.
Factory methodBehaviourBest for
newFixedThreadPool(n)Exactly n threads. If all busy, new tasks wait in an (unbounded) queue.Steady, predictable load; capping concurrency.
newCachedThreadPool()Creates threads as needed, reuses idle ones, +retires threads idle >60s.Lots of short-lived, bursty tasks.
newSingleThreadExecutor()One thread. Tasks run sequentially, in order.Strict ordering; only one task at a time.
newScheduledThreadPool(n)Runs tasks after a delay or repeatedly on a schedule.Timers, polling, cron-like jobs.
new ThreadPoolExecutor(...)Custom core/max size, queue type, keep-alive, rejection policy.When you need real control.

Example construction lines look like:

ExecutorService fixed = Executors.newFixedThreadPool(4);
ExecutorService cached = Executors.newCachedThreadPool();
ExecutorService single = Executors.newSingleThreadExecutor();
ScheduledExecutorService sched = Executors.newScheduledThreadPool(4);

Pitfall

+newFixedThreadPool and newSingleThreadExecutor use an unbounded LinkedBlockingQueue. Under a flood of tasks the queue grows without limit and you get an OutOfMemoryError. newCachedThreadPool has the opposite risk: it can spawn an unbounded number of threads. In production, many teams build a bounded ThreadPoolExecutor instead — this is a frequent interview "gotcha."

Interview angle

"Why not just always use a cached pool?" → Because it's unbounded; a sudden spike creates a thread per queued task and can take the box down. Fixed pools bound threads but can grow the queue; bounded custom pools bound both.

4Manual thread management vs ExecutorService

ExecutorService replaces a pile of bookkeeping (create / start / join / cleanup) with submit-and-shutdown.

Manual way: you keep a List<Thread>, build a new Thread(task) per job, call .start() on each, then loop calling .join() to wait for them all. You own every detail — and there's no built-in way to get a value back.

ExecutorService way: create a pool once, submit() each task, and call shutdown() when done (or shutdownNow() to force-stop if tasks run too long). The pool handles thread lifecycle, reuse, and queuing. It lets multiple tasks run in parallel across multiple threads.

ConcernManual threadsExecutorService
Thread lifecycleYou create/start/join eachPool manages & reuses
Launchthread.start()executor.submit(task)
Wait for completionfor(t: list) t.join();Future / awaitTermination()
Return a valueAwkward (Runnable is void)Natural via Callable + Future
Resource reuseNone — new thread each timeThreads pooled & recycled
TeardownManualshutdown()
Why it matters

Manual threading is error-prone (forgotten joins, leaked threads, no result channel). The executor gives you reuse, a result mechanism, and lifecycle control for free.

Pitfall

+An ExecutorService that you never shutdown() keeps non-daemon worker threads alive, so your program won't exit. Always shut it down — ideally in a finally block or try-with-resources (Java 19+ makes ExecutorService AutoCloseable).

5Runnable — fire-and-forget tasks

A Runnable runs work on another thread but can't hand a result back or surface a checked exception to the caller.

Say I want to fetch weather data from an external API on a background thread so the main thread isn't blocked. With a Runnable, I implement void run() and the task executes off-thread — but run() returns nothing and cannot throw checked exceptions. So getting the fetched data back into the main thread, or propagating a fetch failure, is clumsy: I'd have to stash the result in a shared field and coordinate access myself.

Analogy

Runnable is mailing a letter with no return address. The task gets done somewhere, but nothing comes back to you automatically.

Pitfall

+An exception thrown inside run() doesn't propagate to the submitter — it's swallowed or routed to the thread's uncaught-exception handler. This makes silent failures easy. Callable + Future fixes both problems (result + exception).

6Callable — tasks that return a result

Callable<V> is the result-bearing, exception-throwing cousin of Runnable.

Callable<V> is a functional interface with a single method V call() that returns a result and can throw checked exceptions. It's the right choice whenever a background task needs to produce a value or report a failure that the calling thread should see. It's used together with ExecutorService and Future: you submit a Callable, you get a Future, and you read the result through the Future.

AspectRunnableCallable<V>
Single methodvoid run()V call()
Returns a value?NoYes (V)
Throws checked exceptions?NoYes
Introduced inJava 1.0Java 5
submit() givesFuture<?> (resolves to null)Future<V>
Why it matters

The original question — "how do I get the result from the background fetch into the main thread?" — is answered by use Callable. It's the canonical way to do request/response style async work.

Interview angle

Be ready to state the three differences crisply: return value, checked exceptions, and method name/signature. Bonus: both are functional interfaces, so both can be written as lambdas.

7Future — a handle on a not-yet-finished result

main submit() … free to do other work … get() — blocks result! worker task running in the background… done → get() unblocks time
submit() returns the Future instantly — get() blocks only from when you ask to when the result is ready
A Future is an IOU: submit work now, claim the result later.

The Future<V> interface represents the result of an asynchronous computation. When you submit a task you immediately get a Future back, even though the work may not be done yet. You can then ask the Future to retrieve the result (waiting if necessary), check whether it's finished, or cancel it.

MethodWhat it does
V get()Blocks until the task completes, then returns the result. +Wraps task errors in ExecutionException.
V get(timeout, unit)+Same, but gives up after the timeout with TimeoutException.
boolean isDone()True if the task finished — normally, by exception, or by cancellation.
boolean cancel(mayInterruptIfRunning)Attempts to cancel; returns whether it succeeded.
boolean isCancelled()True if the task was cancelled before completing.
Analogy

A coat-check ticket. You hand over your coat (task), get a stub (Future) right away, and redeem it for the coat (get()) when you're ready — and you may have to wait at the counter.

Pitfall

+get() blocks. Calling it right after submit() serializes your tasks and kills the parallelism you set up. Submit all tasks first, then collect results. Also: get() throws checked InterruptedException and ExecutionException — you must handle them.

+Good to know

CompletableFuture (Java 8) is the modern upgrade: non-blocking chaining with thenApply/thenCompose, combining, and callbacks. Plain Future only lets you block on get() or poll isDone().

8Task submission: submit / invokeAll / invokeAny

Three ways to hand work to a pool, differing in how many tasks and what they wait for.

These methods control how tasks are queued, executed, and how results come back:

MethodInputBehaviourReturns
submit(task)One Runnable/CallableSchedules it; returns immediately (non-blocking).Future<V>
invokeAll(tasks)Collection of CallablesBlocks until ALL finish.List<Future<V>>
invokeAny(tasks)Collection of CallablesReturns the FIRST successful result, cancels the rest.V (the result itself)

Use submit for individual tasks whose results you'll fetch via Future. Use invokeAll when you need every task's result (a fan-out where all answers matter). Use invokeAny when any one answer is enough and you want the fastest — e.g. querying three mirror servers and taking whichever replies first.

Analogy

invokeAll = wait for the whole class to finish the exam. invokeAny = a race; the first runner across the line wins and the others are pulled off the track.

Interview angle

Remember the return-type asymmetry: invokeAll gives you a list of Futures, but invokeAny gives you the raw result value (no Future), because there's only one winner.

9Shutting the pool down

+A pool you forget to close keeps your program alive forever.

+This is implied by the shutdown() / shutdownNow() calls but worth stating plainly. Pool threads are non-daemon by default, so the JVM won't exit while they exist.

Why it matters

The standard, correct teardown is: shutdown() → awaitTermination(...) → if it times out, shutdownNow(). This guarantees clean exit without abandoning in-flight work prematurely.

↳ See code: Code §A and the Lab Setup

Connections: Need (1) motivates pools (2–4). To get results out you move Runnable (5) → Callable (6) → Future (7). Submission methods (8) are how Callables enter the pool. Shutdown (9) closes the loop. The whole chain is: create pool → submit Callables → collect via Future → shutdown.

II

Code Samples

Runnable Java (standard library only, Java 17+). Each block: filename · run command · expected output. + marks code I added to complete a referenced-but-unshown idea.

AManual threads vs ExecutorService

Same workload (4 print-and-sleep tasks) done both ways. +Reconstructed runnable versions of both snippets.

// ManualThreads.java
import java.util.*;

public class ManualThreads {
  public static void main(String[] args) throws InterruptedException {
    List<Thread> workers = new ArrayList<>();
    for (int i = 1; i <= 4; i++) {
            final int id = i;
            // Runnable = the task; one new Thread object per task
            Thread t = new Thread(() -> System.out.println("Task " + id + " on " + Thread.currentThread().getName()));
            workers.add(t);
    }
    workers.forEach(Thread::start);   // must start each thread
    for (Thread t : workers) t.join(); // must join each to wait
    System.out.println("All manual tasks done");
  }
}
$ javac ManualThreads.java && java ManualThreads
EXPECTED OUTPUT (order varies)Task 1 on Thread-0 Task 3 on Thread-2 Task 2 on Thread-1 Task 4 on Thread-3 All manual tasks done
// PoolDemo.java
import java.util.concurrent.*;

public class PoolDemo {
  public static void main(String[] args) throws InterruptedException {
    // ONE pool of 4 reusable threads — no per-task Thread objects
    ExecutorService executor = Executors.newFixedThreadPool(4);
    for (int i = 1; i <= 4; i++) {
            final int id = i;
            executor.submit(() -> System.out.println("Task " + id + " on " + Thread.currentThread().getName()));
    }
    executor.shutdown();                              // graceful: no new tasks
    executor.awaitTermination(5, TimeUnit.SECONDS); // wait for finish
    System.out.println("All pool tasks done");
  }
}
$ javac PoolDemo.java && java PoolDemo
EXPECTED OUTPUT (order varies)Task 1 on pool-1-thread-1 Task 2 on pool-1-thread-2 Task 4 on pool-1-thread-4 Task 3 on pool-1-thread-3 All pool tasks done
Why this output

Manual threads are named Thread-0…3; pooled ones are pool-1-thread-1…4. Ordering is non-deterministic because the OS scheduler decides who runs when. Without shutdown(), PoolDemo would never exit — the four pool threads stay alive.

BCreating the pool types

// PoolTypes.java
import java.util.concurrent.*;

public class PoolTypes {
  public static void main(String[] args) {
    ExecutorService fixed   = Executors.newFixedThreadPool(4);    // 4 fixed workers
    ExecutorService cached  = Executors.newCachedThreadPool();      // grows/shrinks on demand
    ExecutorService single  = Executors.newSingleThreadExecutor();  // strictly sequential
    ScheduledExecutorService sched = Executors.newScheduledThreadPool(2);

    single.submit(() -> System.out.println("sequential task"));

    // scheduled: run once after a 1-second delay
    sched.schedule(() -> System.out.println("ran after 1s delay"), 1, TimeUnit.SECONDS);

    // always shut every pool down
    fixed.shutdown(); cached.shutdown(); single.shutdown();
    sched.schedule(sched::shutdown, 2, TimeUnit.SECONDS); // let the delayed task fire first
  }
}
$ javac PoolTypes.java && java PoolTypes
EXPECTED OUTPUTsequential task ran after 1s delay (appears ~1 second later)
Note

+A ScheduledExecutorService ignores plain shutdown() for already-scheduled future tasks oddly in edge cases; here we self-schedule the shutdown so the delayed task isn't cut off. In real code, prefer awaitTermination.

CWeather fetcher — Runnable vs Callable + Future

+The source references WeatherFetcherRunnable.java and WeatherFetcherCallable.java but doesn't show the bodies. These are faithful, self-contained reconstructions (a fake fetch stands in for the real API call so they run with zero setup).

// WeatherFetcherRunnable.java  — the LIMITATION demo
import java.util.concurrent.*;

public class WeatherFetcherRunnable implements Runnable {
  private volatile String result; // must stash result in shared state

  @Override
  public void run() {
    // run() returns void → can't hand a value back directly
    // run() can't throw checked exceptions → must swallow/wrap
    try { Thread.sleep(300); } catch (InterruptedException e) { return; }
    result = "Weather: 28C, clear";
  }
  public String getResult() { return result; }

  public static void main(String[] args) throws InterruptedException {
    WeatherFetcherRunnable task = new WeatherFetcherRunnable();
    Thread t = new Thread(task);
    t.start();
    t.join();                          // must wait, THEN read shared field
    System.out.println(task.getResult());
  }
}
$ javac WeatherFetcherRunnable.java && java WeatherFetcherRunnable
EXPECTED OUTPUTWeather: 28C, clear
Why it's awkward

The result travels through a shared volatile field and a manual join(). There's no clean way to propagate a fetch failure to main — exceptions in run() don't reach the caller.

// WeatherFetcherCallable.java  — the CLEAN way
import java.util.concurrent.*;

public class WeatherFetcherCallable implements Callable<String> {
  @Override
  public String call() throws Exception {  // returns a value + can throw checked exc
    Thread.sleep(300);                       // pretend network latency
    if (Math.random() < 0) throw new Exception("API down"); // could throw freely
    return "Weather: 28C, clear";            // result flows back via Future
  }

  public static void main(String[] args) throws Exception {
    ExecutorService ex = Executors.newSingleThreadExecutor();
    Future<String> future = ex.submit(new WeatherFetcherCallable()); // non-blocking
    System.out.println("Doing other work while fetch runs...");
    System.out.println("isDone? " + future.isDone());      // likely false
    String weather = future.get();   // BLOCKS until result ready; rethrows as ExecutionException
    System.out.println(weather);
    ex.shutdown();
  }
}
$ javac WeatherFetcherCallable.java && java WeatherFetcherCallable
EXPECTED OUTPUTDoing other work while fetch runs... isDone? false Weather: 28C, clear
The key contrast

Callable returns the value through the Future — no shared field, no manual join. If call() threw, future.get() would raise an ExecutionException wrapping it, so failures reach the caller cleanly.

DApp.java — submit / invokeAll / invokeAny

// App.java
import java.util.*;
import java.util.concurrent.*;

public class App {
  public static void main(String[] args) throws Exception {
    ExecutorService ex = Executors.newFixedThreadPool(3);

    // three Callables that return after different delays
    List<Callable<String>> tasks = List.of(() -> { Thread.sleep(300); return "A (300ms)"; },() -> { Thread.sleep(100); return "B (100ms)"; },() -> { Thread.sleep(200); return "C (200ms)"; }
    );

    // 1) submit — fire one task, get a Future, don't block
    Future<String> f = ex.submit(() -> "single submit result");
    System.out.println("submit  -> " + f.get());

    // 2) invokeAll — blocks until ALL done; returns a Future per task
    List<Future<String>> all = ex.invokeAll(tasks);
    System.out.print("invokeAll -> ");
    for (Future<String> ft : all) System.out.print(ft.get() + "  ");
    System.out.println();

    // 3) invokeAny — returns the FIRST successful result (the fastest, B)
    String first = ex.invokeAny(tasks);
    System.out.println("invokeAny -> " + first);

    ex.shutdown();
  }
}
$ javac App.java && java App
EXPECTED OUTPUTsubmit -> single submit result invokeAll -> A (300ms) B (100ms) C (200ms) (list order preserved) invokeAny -> B (100ms) (fastest wins; others cancelled)
Why this output

invokeAll returns Futures in the input order regardless of finish order, so you get A, B, C. invokeAny returns whichever finishes first — B at 100 ms — and cancels A and C. Note invokeAny returns a bare String, not a Future.

III

Hands-On Lab

Predict the output before you run it — that's where the learning is.

⚙One-time Setup

# 1. Check Java is installed (need 17+; any 8+ works for most of this)
java -version
javac -version

# 2. Make a working folder
mkdir -p ~/workspace/java-threads/lab && cd ~/workspace/java-threads/lab

# 3. Compile + run pattern used throughout
#    (each file has a public static void main)
javac FileName.java
java  FileName
EXPECTED FROM java -versionopenjdk version "17.x.x" or higher

+No external libraries needed — everything is in java.util.concurrent. One class per file, filename must match the public class name.

Warm-up

1 · Pool vs no pool, see the thread names

Goal: observe that a pool reuses a small set of named threads.
  1. Create NameDemo.java with a newFixedThreadPool(2).
  2. Submit 6 tasks, each printing Thread.currentThread().getName().
  3. shutdown() the pool.
Predict: How many distinct thread names will appear across 6 tasks? Then run and verify.
Solution
// NameDemo.java
import java.util.concurrent.*;
public class NameDemo {
  public static void main(String[] a) {
    ExecutorService ex = Executors.newFixedThreadPool(2);
    for (int i = 1; i <= 6; i++) {
            final int id = i;
            ex.submit(() -> System.out.println("task " + id + " -> " + Thread.currentThread().getName()));
    }
    ex.shutdown();
  }
}
EXPECTED (order varies)task 1 -> pool-1-thread-1 task 2 -> pool-1-thread-2 task 3 -> pool-1-thread-1 ... only TWO distinct names appear — the pool reuses them.
Warm-up

2 · Make a Callable return a value

Goal: get a computed result back through a Future.
  1. Write a Callable<Integer> (lambda) that returns the sum 1..100.
  2. Submit it to a single-thread executor, capture the Future<Integer>.
  3. Print future.get(), then shut down.
Predict: What number prints? What type does get() return here?
Solution
// SumCallable.java
import java.util.concurrent.*;
public class SumCallable {
  public static void main(String[] a) throws Exception {
    ExecutorService ex = Executors.newSingleThreadExecutor();
    Future<Integer> f = ex.submit(() -> {
            int s = 0;
            for (int i = 1; i <= 100; i++) s += i;
            return s;                      // Callable can return
    });
    System.out.println("sum = " + f.get()); // blocks, returns Integer
    ex.shutdown();
  }
}
EXPECTED OUTPUTsum = 5050
Core

3 · Feel the blocking trap of get()

Goal: see how calling get() too early destroys parallelism.
  1. Create 3 Callables that each sleep(1000) then return their id.
  2. Version A: submit one, get() it, submit next, get() it... Time the total.
  3. Version B: submit all 3 first into a list of Futures, then loop get(). Time it.
Predict: Roughly how long does A take? How long does B (pool size 3)? Why the difference?
Solution
// BlockingTrap.java
import java.util.*;
import java.util.concurrent.*;
public class BlockingTrap {
  static Callable<Integer> job(int id) {
    return () -> { Thread.sleep(1000); return id; };
  }
  public static void main(String[] a) throws Exception {
    ExecutorService ex = Executors.newFixedThreadPool(3);

    long t0 = System.currentTimeMillis();          // --- Version A: get() each time ---
    for (int i = 1; i <= 3; i++) ex.submit(job(i)).get();
    System.out.println("A took ~" + (System.currentTimeMillis() - t0) + "ms");

    long t1 = System.currentTimeMillis();          // --- Version B: submit all, then get ---
    List<Future<Integer>> fs = new ArrayList<>();
    for (int i = 1; i <= 3; i++) fs.add(ex.submit(job(i)));
    for (Future<Integer> f : fs) f.get();
    System.out.println("B took ~" + (System.currentTimeMillis() - t1) + "ms");
    ex.shutdown();
  }
}
EXPECTED OUTPUT (approx)A took ~3000ms // serialized: get() waits before next submit B took ~1000ms // parallel: all 3 ran at once on 3 threads

Lesson: submit everything, then collect. get() is a barrier.

Core

4 · invokeAll — collect every result

Goal: run several Callables and read all their results in input order.
  1. Build a List<Callable<String>> of 3 tasks with different sleep times (e.g. 300/100/200 ms).
  2. Call invokeAll(tasks).
  3. Loop the returned Futures and print each get().
Predict: Will results print in finish order (fastest first) or in the order you listed the tasks?
Solution
// InvokeAllDemo.java
import java.util.*;
import java.util.concurrent.*;
public class InvokeAllDemo {
  public static void main(String[] a) throws Exception {
    ExecutorService ex = Executors.newFixedThreadPool(3);
    List<Callable<String>> tasks = List.of(() -> { Thread.sleep(300); return "slow-300"; },() -> { Thread.sleep(100); return "fast-100"; },() -> { Thread.sleep(200); return "mid-200";  }
    );
    List<Future<String>> results = ex.invokeAll(tasks); // blocks till ALL done
    for (Future<String> f : results) System.out.println(f.get());
    ex.shutdown();
  }
}
EXPECTED OUTPUTslow-300 fast-100 mid-200 // INPUT order, not finish order — Futures map 1:1 to the task list
Challenge

5 · invokeAny — race the mirrors

Goal: get only the fastest result and prove the slow ones got cancelled.
  1. Three Callables: fast (100 ms), medium (400 ms), slow (800 ms). Each prints "started" and "finished".
  2. Call invokeAny(tasks) and print the single returned value.
  3. Observe which tasks print "finished".
Predict: Which value comes back? Do the slow tasks print "finished"? What does invokeAny return type-wise?
Solution
// InvokeAnyDemo.java
import java.util.*;
import java.util.concurrent.*;
public class InvokeAnyDemo {
  static Callable<String> mirror(String name, int ms) {
    return () -> {
            System.out.println(name + " started");
            Thread.sleep(ms);
            System.out.println(name + " finished");  // cancelled tasks may never reach this
            return name + " result";
    };
  }
  public static void main(String[] a) throws Exception {
    ExecutorService ex = Executors.newFixedThreadPool(3);
    List<Callable<String>> tasks = List.of(mirror("fast", 100), mirror("medium", 400), mirror("slow", 800));
    String winner = ex.invokeAny(tasks);   // returns String, not Future
    System.out.println("WINNER: " + winner);
    ex.shutdownNow();                       // stop the losers immediately
  }
}
EXPECTED OUTPUT (approx)fast started medium started slow started fast finished WINNER: fast result // medium/slow are cancelled — usually no "finished" line for them

invokeAny returns the bare value (here a String) and cancels the rest as soon as one succeeds.

Challenge

6 · Handle a failing task + clean shutdown

Goal: see how a Callable's exception surfaces through get(), and do a textbook shutdown.
  1. Submit a Callable that throws new IllegalStateException("boom").
  2. Wrap future.get() in try/catch for ExecutionException; print the cause.
  3. Shut down with the pattern: shutdown() → awaitTermination(2s) → shutdownNow() if it times out.
Predict: What exception type does get() throw — IllegalStateException or something wrapping it? What is e.getCause()?
Solution
// FailAndShutdown.java
import java.util.concurrent.*;
public class FailAndShutdown {
  public static void main(String[] a) throws InterruptedException {
    ExecutorService ex = Executors.newFixedThreadPool(2);
    Future<String> f = ex.submit(() -> { throw new IllegalStateException("boom"); });

    try {
            f.get();                                // re-throws task error wrapped
    } catch (ExecutionException e) {
            System.out.println("get() threw: " + e.getClass().getSimpleName());
            System.out.println("real cause : " + e.getCause()); // the IllegalStateException
    }

    ex.shutdown();                            // graceful
    if (!ex.awaitTermination(2, TimeUnit.SECONDS)) {
            ex.shutdownNow();                       // forceful fallback
    }
    System.out.println("clean exit");
  }
}
EXPECTED OUTPUTget() threw: ExecutionException real cause : java.lang.IllegalStateException: boom clean exit

The task's exception is always wrapped in ExecutionException; unwrap with getCause(). This is exactly why Callable beats Runnable for error handling.

✓Self-check rubric — how to know you've got it

  • ✓I can explain why a thread pool beats one-thread-per-task in 2 sentences (reuse, bounded cost).
  • ✓Given a scenario, I can pick the right pool type (fixed / cached / single / scheduled) and justify it.
  • ✓I can state the 3 differences between Runnable and Callable without looking.
  • ✓I know that get() blocks, and why submitting-all-then-getting preserves parallelism (Exercise 3).
  • ✓I can predict that invokeAll returns Futures in input order and invokeAny returns one raw value.
  • ✓I can write the canonical lifecycle from memory: create → submit → get → shutdown.
  • ✓I know a task's exception arrives wrapped in ExecutionException and how to unwrap it.
  • ✓I never forget to shutdown(), and I know why a program hangs if I do.
★

Key Takeaways

The whole session in nine lines.
✎

Recall Card

Copy-by-hand cheat sheet. Telegraphic. Every line = max recall, min ink.
✎
You’ve read the story and seen the diagrams above — this is the condensed map to copy into your notebook for fast recall before an interview. Pure shorthand, no new ideas.
// WHY POOL THREADS
THREAD POOL = team of trained workers on standby problem → 1 new thread / task = slow + costly + chaos at peak fix → reuse a fixed crew of ready threads wins → instant start · smooth peaks · no create/destroy churn cost → threads idle when no work · must SHUTDOWN or JVM hangs
// EXECUTOR FRAMEWORK STACK
Executor (run) ⊂ ExecutorService (submit, shutdown, invoke*) ⊂ ScheduledExecutorService (schedule) Executors = factory class (static methods) → builds pools ThreadPoolExecutor = the real impl (full manual control)
// POOL TYPES (Executors factory)
TypeThreadsUse when
newFixedThreadPool(n)exactly n; extras queue & waitsteady load, cap concurrency
newCachedThreadPool()grows on demand, reuses idle, kills idle>60smany short bursty tasks
newSingleThreadExecutor()exactly 1, sequentialstrict order, one-at-a-time
newScheduledThreadPool(n)n, delayed/periodictimers, cron-like jobs
new ThreadPoolExecutor(...)fully customneed queue/size/reject control
// MANUAL THREADS vs EXECUTORSERVICE
Manual new Thread()ExecutorService
create1 Thread obj / taskpool reused
start.start() each.submit(task)
wait.join() loopFuture / awaitTermination
resulthard (Runnable=void)Future from Callable
cleanupyou manageshutdown()
// RUNNABLE vs CALLABLE
RunnableCallable<V>
methodvoid run()V call()
returnsnothinga result (V)
checked exccan't throwcan throw
sinceJava 1.0Java 5
with executorsubmit→Future<?> (null)submit→Future<V>
mnemonic: Callable = Carries result + Can throw. Runnable = run & forget.
// FUTURE — handle on a not-yet-done result
Future<V> = IOU for an async result get() → blocks till done, returns V (throws ExecutionException) get(t,unit) → blocks max t, else TimeoutException isDone() → finished? (done OR cancelled OR failed) cancel(mayInt) → try cancel; true if cancelled isCancelled() → was it cancelled? mem: get() = "are we there yet?" — it WAITS
// SUBMISSION METHODS
MethodTakesReturns / waits for
submit(task)1 taskFuture; does NOT block
invokeAll(tasks)collectionList<Future>; blocks till ALL done
invokeAny(tasks)collection1 result, FIRST success; cancels rest
All = wait for everyone · Any = race, first finisher wins
// SHUTDOWN (don't forget!)
shutdown() → no new tasks; finish queued ones (graceful) shutdownNow() → interrupt running, drop queued (forceful) awaitTermination(t,unit) → block till done or timeout rule: forget shutdown → non-daemon pool threads keep JVM alive
// CANONICAL FLOW (memorize the shape)
ExecutorService ex = Executors.newFixedThreadPool(4); Future<T> f = ex.submit(callable); // fire T result = f.get(); // block + collect ex.shutdown(); // always