Executors & Thread Pools
Higher-level concurrency from java.util.concurrent.
At a Glance
ExecutorService— Core abstraction for managing a pool of threads. Submit tasks, get Futures, shut down gracefully. Always shut down executors you create.Executors.newFixedThreadPool(n)— Fixed-size pool. Best for CPU-bound work where you know the parallelism level. Unbounded task queue — watch for memory if producers outpace consumers.Executors.newCachedThreadPool()— Creates threads on demand, reuses idle ones (60s TTL). Good for short-lived I/O tasks. Can create unbounded threads under load.Executors.newSingleThreadExecutor()— Single worker thread with an unbounded queue. Tasks execute sequentially in submission order. Useful for serializing access to a resource.Executors.newScheduledThreadPool(n)— Fixed pool that supports delayed and periodic task execution. Use for scheduled jobs, retries, and timeouts.ThreadPoolExecutor— The configurable implementation behind most factory methods. Tune core/max pool size, queue type, rejection policy, and keep-alive time.ForkJoinPool— Work-stealing pool for recursive divide-and-conquer tasks. Powers parallel streams andCompletableFutureby default.Future<V>— Handle to an async result.get()blocks until complete. Limited composability — useCompletableFuturefor chaining.CompletableFuture<V>— Composable async programming. Chain transformations, combine results, handle errors — all non-blocking. The modern way to write async Java.
ExecutorService
Submit tasks, manage lifecycle.
ExecutorService pool = Executors.newFixedThreadPool(4);
// Submit a Runnable (no return value)
pool.execute(() -> doWork());
// Submit a Callable (returns a Future)
Future<String> future = pool.submit(() -> fetchData());
String result = future.get(); // blocks until done
// With timeout
String result = future.get(5, TimeUnit.SECONDS);
// Check status
future.isDone();
future.isCancelled();
future.cancel(true); // true = interrupt if running Shutdown
// Graceful shutdown — finish running tasks, reject new ones
pool.shutdown();
// Wait for completion
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow(); // cancel running tasks
}
// Idiomatic shutdown pattern
pool.shutdown();
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
}
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
// try-with-resources (Java 19+ — AutoCloseable)
try (var pool = Executors.newFixedThreadPool(4)) {
pool.submit(() -> doWork());
} // calls shutdown + awaitTermination Thread Pool Types
| Factory Method | Core | Max | Queue | Best For |
|---|---|---|---|---|
newFixedThreadPool(n) | n | n | Unbounded LinkedBlockingQueue | CPU-bound work with known parallelism |
newCachedThreadPool() | 0 | Integer.MAX_VALUE | SynchronousQueue | Many short-lived I/O tasks |
newSingleThreadExecutor() | 1 | 1 | Unbounded LinkedBlockingQueue | Sequential task execution |
newScheduledThreadPool(n) | n | Integer.MAX_VALUE | DelayedWorkQueue | Delayed and periodic tasks |
newWorkStealingPool() | Runtime CPUs | Runtime CPUs | Per-thread deques | Parallel recursive/divide-and-conquer work |
Custom ThreadPoolExecutor
When the factory methods don't fit.
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, // corePoolSize
8, // maximumPoolSize
60L, TimeUnit.SECONDS, // keepAliveTime for excess threads
new ArrayBlockingQueue<>(100), // bounded work queue
new ThreadPoolExecutor.CallerRunsPolicy() // rejection policy
); Rejection Policies
| Policy | Behavior |
|---|---|
AbortPolicy (default) | Throws RejectedExecutionException |
CallerRunsPolicy | Runs the task in the submitting thread (backpressure) |
DiscardPolicy | Silently drops the task |
DiscardOldestPolicy | Drops the oldest queued task, retries submission |
ScheduledExecutorService
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// Run once after delay
scheduler.schedule(() -> doWork(), 5, TimeUnit.SECONDS);
// Repeat at fixed rate (period measured from start of each run)
scheduler.scheduleAtFixedRate(() -> poll(), 0, 10, TimeUnit.SECONDS);
// Repeat with fixed delay (delay measured from end of each run)
scheduler.scheduleWithFixedDelay(() -> poll(), 0, 10, TimeUnit.SECONDS);
// Cancel a scheduled task
ScheduledFuture<?> handle = scheduler.scheduleAtFixedRate(() -> poll(), 0, 10, TimeUnit.SECONDS);
handle.cancel(false); Bulk Submission
ExecutorService pool = Executors.newFixedThreadPool(4);
List<Callable<String>> tasks = List.of(
() -> fetch("url1"),
() -> fetch("url2"),
() -> fetch("url3")
);
// invokeAll — wait for all to complete
List<Future<String>> futures = pool.invokeAll(tasks);
for (Future<String> f : futures) {
System.out.println(f.get());
}
// invokeAny — return first successful result, cancel the rest
String fastest = pool.invokeAny(tasks); CompletableFuture
Composable async programming. Java 8+.
Creating
// Async task on ForkJoinPool.commonPool()
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> fetchData());
// With custom executor
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> fetchData(), myPool);
// Already-complete values
CompletableFuture<String> done = CompletableFuture.completedFuture("hello");
CompletableFuture<String> failed = CompletableFuture.failedFuture(new RuntimeException("oops")); Chaining
CompletableFuture.supplyAsync(() -> fetchUser(id))
.thenApply(user -> user.email()) // transform
.thenCompose(email -> sendEmailAsync(email)) // flatMap (returns CF)
.thenAccept(result -> log(result)) // consume (void)
.thenRun(() -> cleanup()) // run after (no input)
.exceptionally(ex -> { // recover from error
logError(ex);
return fallback;
}); Combining
// Combine two futures
CompletableFuture<String> nameF = CompletableFuture.supplyAsync(() -> fetchName());
CompletableFuture<Integer> ageF = CompletableFuture.supplyAsync(() -> fetchAge());
CompletableFuture<String> combined = nameF.thenCombine(ageF,
(name, age) -> name + " is " + age);
// Wait for all
CompletableFuture<Void> all = CompletableFuture.allOf(cf1, cf2, cf3);
all.join(); // blocks until all complete
// Race — first to complete wins
CompletableFuture<Object> first = CompletableFuture.anyOf(cf1, cf2, cf3); Error Handling
cf.handle((result, ex) -> {
if (ex != null) {
return fallback;
}
return transform(result);
});
// whenComplete — observe result/error without transforming
cf.whenComplete((result, ex) -> {
if (ex != null) log(ex);
}); Async Variants
| Sync (same thread) | Async (ForkJoinPool) | Async (custom executor) |
|---|---|---|
thenApply | thenApplyAsync | thenApplyAsync(fn, exec) |
thenAccept | thenAcceptAsync | thenAcceptAsync(fn, exec) |
thenCompose | thenComposeAsync | thenComposeAsync(fn, exec) |
thenRun | thenRunAsync | thenRunAsync(fn, exec) |
ForkJoinPool
Work-stealing pool for recursive divide-and-conquer. Powers parallel streams.
class SumTask extends RecursiveTask<Long> {
private final long[] arr;
private final int lo, hi;
static final int THRESHOLD = 1000;
SumTask(long[] arr, int lo, int hi) {
this.arr = arr; this.lo = lo; this.hi = hi;
}
@Override
protected Long compute() {
if (hi - lo <= THRESHOLD) {
long sum = 0;
for (int i = lo; i < hi; i++) sum += arr[i];
return sum;
}
int mid = (lo + hi) / 2;
SumTask left = new SumTask(arr, lo, mid);
SumTask right = new SumTask(arr, mid, hi);
left.fork(); // async execute left
long rightResult = right.compute(); // compute right in this thread
long leftResult = left.join(); // wait for left
return leftResult + rightResult;
}
}
ForkJoinPool pool = new ForkJoinPool();
long total = pool.invoke(new SumTask(data, 0, data.length));
// Common pool (shared, used by parallel streams)
ForkJoinPool.commonPool();