Executors, futures and thread pools
An ExecutorService decouples task submission from the threads that run them. Future is a handle on a pending result; CompletableFuture adds composition without blocking.
Creating threads by hand does not bound concurrency. A pool does, and it also lets you name threads, which is the difference between a readable thread dump and an unreadable one.
ExecutorService pool = Executors.newFixedThreadPool(8);
Future<Integer> f = pool.submit(() -> compute());
Integer result = f.get(); // blocks
pool.shutdown();
Choosing a pool
- Fixed — bounded, backed by an unbounded queue. Steady CPU-bound work.
- Cached — unbounded thread count. Fine for short I/O tasks, dangerous under load.
- Scheduled — periodic execution.
- Work-stealing / ForkJoin — recursive divide-and-conquer.
For anything production-facing, construct ThreadPoolExecutor directly so you
control the queue bound and the rejection policy. An unbounded queue turns
overload into an OutOfMemoryError instead of backpressure.
Shutdown
shutdown() stops accepting work and drains; shutdownNow() interrupts
running tasks and returns those never started. Non-daemon pool threads keep the
JVM alive, so a missing shutdown is a hang.
submit() hides exceptions
An exception thrown inside submit() is captured in the Future and vanishes
unless someone calls get(). With execute() it reaches the thread’s
uncaught-exception handler. This asymmetry swallows a lot of errors.
Composition
CompletableFuture
.supplyAsync(this::fetch, pool)
.thenApply(this::parse)
.exceptionally(e -> fallback())
.thenAccept(this::store);