Executors & Thread Pools

Higher-level concurrency from java.util.concurrent.

At a Glance

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 MethodCoreMaxQueueBest 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

PolicyBehavior
AbortPolicy (default)Throws RejectedExecutionException
CallerRunsPolicyRuns the task in the submitting thread (backpressure)
DiscardPolicySilently drops the task
DiscardOldestPolicyDrops 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)
thenApplythenApplyAsyncthenApplyAsync(fn, exec)
thenAcceptthenAcceptAsyncthenAcceptAsync(fn, exec)
thenComposethenComposeAsyncthenComposeAsync(fn, exec)
thenRunthenRunAsyncthenRunAsync(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();