How it works
The explanation, with examples you can run.
Executors
An executor is a set of worker threads plus a queue. The Executors factory methods cover common shapes, but read what they do:
newFixedThreadPool(n):nthreads and an unbounded queue. Under overload, tasks pile up in memory and latency grows without limit.newCachedThreadPool(): reuses idle threads but creates new ones without limit. A burst can mean thousands of threads.newVirtualThreadPerTaskExecutor(): a virtual thread per task (Java 21, next topic).
For services, build the pool you mean:
ExecutorService pool = new ThreadPoolExecutor( 8, 8, // core and maximum threads 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(500), // bounded queue new ThreadPoolExecutor.CallerRunsPolicy()); // when full, the submitter runs the taskWhen the queue is full the policy decides: throw RejectedExecutionException (the default), run the task on the caller's thread (which slows the producer down), or discard. Since Java 19 an ExecutorService is AutoCloseable: close() stops accepting tasks and waits for the submitted ones, so try-with-resources scopes a pool neatly.
CompletableFuture
import java.util.concurrent.*; public class Prices { static int price(String shop) { sleep(100); // stands in for a slow remote call return shop.length() * 10; } public static void main(String[] args) { try (ExecutorService pool = Executors.newFixedThreadPool(4)) { CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> price("north"), pool); CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> price("south-east"), pool); int best = a.thenCombine(b, Math::min) // both run at once; combine when both finish .orTimeout(2, TimeUnit.SECONDS) // fail if it takes too long .exceptionally(e -> -1) // fallback on any failure .join(); System.out.println("best price: " + best); // best price: 50 } } static void sleep(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { throw new RuntimeException(e); } }}The main combinators:
thenApply(f): transform the value (likemap).thenCompose(f): chain a step that returns another future (likeflatMap).thenCombine(other, f): wait for two independent futures;allOfandanyOffor many.exceptionally,handle,whenComplete: deal with failure. An exception in any step skips the following value steps and arrives wrapped in aCompletionException.orTimeoutandcompleteOnTimeout(Java 9): put a deadline on the whole chain.
Methods ending in Async without an executor argument run on ForkJoinPool.commonPool(), which the whole JVM shares, parallel streams included. It is sized for CPU work, roughly one thread per core, so blocking calls there starve everything else. Pass your own executor for anything that waits on I/O.
Other tools in the box
ConcurrentHashMap for shared maps, BlockingQueue for producer and consumer hand-offs, CountDownLatch and Semaphore for coordination, ReentrantLock when you need timeouts or try-lock that synchronized lacks. Prefer them to hand-written wait and notify.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.