ExerciseChallenge
Challenge
Give the pool a voice
20 minsenior3–12 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- submit() stores the exception in a Future; nothing logs it
- execute() lets it reach the thread's uncaught-exception handler
- Naming pool threads is what makes a thread dump readable
- A rejection policy is a decision — CallerRunsPolicy turns rejection into backpressure
Starter
Starter.javaOpen in playground
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/**
* CHALLENGE — 20 minutes.
*
* Four tasks run. Two of them throw. Count how many failures you can see in
* the output, then go and find the other one.
*
* TASKS
* 1. Run it. One failure is visible and one is not. Which, and why?
* 2. Give the pool a ThreadFactory that names its threads and installs an
* uncaught-exception handler. Run again.
* 3. One of the two is STILL invisible. Work out why the handler never
* fires for it, and fix that task so its failure is observable.
* 4. Swap AbortPolicy for CallerRunsPolicy and describe, in a comment,
* which thread ends up running the rejected task and what that does to
* the code submitting the work.
*/
public class Starter {
static void riskyCleanup() {
throw new IllegalStateException("cleanup failed, and nobody will know");
}
static void riskyReport() {
throw new IllegalStateException("report failed, loudly");
}
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
1, 1, 0, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy()); // (4) try CallerRunsPolicy
// TASK 2: a ThreadFactory belongs in that constructor. Something like
// new ThreadFactory() { public Thread newThread(Runnable r) { ... } }
// naming each thread and calling setUncaughtExceptionHandler.
System.out.println("── running four tasks, two of which throw ──");
pool.execute(() -> System.out.println(" ok : task 1 did its work"));
pool.execute(Starter::riskyReport); // visible?
pool.submit(Starter::riskyCleanup); // visible?
pool.execute(() -> System.out.println(" ok : task 4 did its work"));
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
System.out.println();
System.out.println("failures you could see above: ___ of 2");
// Question to answer in a comment before you move on:
// Both failing tasks throw the same exception from the same pool.
// What is different about how they were handed to it?
}
}Run it locally:
cd exercises/java/concurrency/executor-service/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
Run it. Two of the four tasks fail. Count how many failures you can see.
Hint 2
Look at what submit() returns and what the code does with it.
Hint 3
A ThreadFactory can set both the name and an uncaught-exception handler. Which of those two helps the submit() case?
Hint 4
For the last task: compare AbortPolicy and CallerRunsPolicy by which thread ends up doing the work, and what that does to the producer.
Done when
- Every failing task is visible in the output — none silent
- Pool threads have meaningful names
- A comment explains why the ThreadFactory's handler did not fix the submit() case
- Both rejection policies are tried, and the difference is described
Stretch
Wrap the pool so that submit() cannot silently swallow anything: override
afterExecute(Runnable, Throwable) in a ThreadPoolExecutor subclass and
recover the exception from the Future when the Throwable argument is null.
That is the only place both cases converge. Then decide whether you would
ship it, or just ban submit() for fire-and-forget in review.
← Back to How do you size a thread pool, and what does an unbounded queue cost you?