ExecutorService

Submit tasks to thread pools, collect results with Future, bound queues and shut pools down properly.

Syntaxtry (ExecutorService pool = Executors.newFixedThreadPool(n)) { Future<T> f = pool.submit(callable); T result = f.get(timeout, unit); }

An ExecutorService separates what to run from how it runs. You submit tasks; the executor decides which thread runs them, reuses threads between tasks and queues work when all threads are busy. Almost every server, scheduler and HTTP client in the Java ecosystem is built on one.

Submitting work

execute(Runnable) fires and forgets. submit(Callable) returns a Future, a handle to a result that may not exist yet. future.get() blocks until the task is done and rethrows a failure wrapped in ExecutionException. invokeAll runs a batch and returns the futures in the same order as the tasks, which keeps output stable.

try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
    Future<Quote> quote = pool.submit(() -> fetchQuote("acme"));
    Quote q = quote.get(2, TimeUnit.SECONDS);
}   // close() shuts the pool down, then waits for submitted tasks to finish

Since Java 19 ExecutorService is AutoCloseable, so try-with-resources shuts it down and waits. Java 19 also added Future.state(), resultNow() and exceptionNow() for inspecting finished futures without checked exceptions.

Choosing a pool

  • newFixedThreadPool(n): n threads and an unbounded queue. Good for CPU-bound work sized to the core count.
  • newCachedThreadPool(): creates threads without limit. A traffic spike can create thousands of platform threads and exhaust memory.
  • newVirtualThreadPerTaskExecutor(): a new virtual thread per task, ideal for blocking I/O.
  • new ThreadPoolExecutor(...): full control over core size, maximum size, queue capacity and what happens when everything is full.

Mistakes that hurt in production

  • Never shutting the pool down. Its non-daemon threads keep the JVM alive and the process hangs on exit.
  • Calling get() without a timeout on a remote call. One stuck dependency then blocks a request thread forever.
  • Unbounded queues in front of slow work. The queue grows until memory runs out, and requests wait minutes before failing. A bounded queue with a rejection policy lets you answer 503 quickly instead.
  • Ignoring the Future of a task that throws. The exception sits inside the future and nobody sees it.

A timed-out get() does not stop the task. Call cancel(true) to interrupt it, otherwise it keeps using a thread and the downstream connection.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Call three supplier pricing APIs at the same time and return the cheapest quote within one request.
  • Run a batch of health checks with invokeAll and report each dependency's status on a /health endpoint.
  • Protect a slow PDF export with a bounded ThreadPoolExecutor that answers 503 when the queue is full.

More examples

invokeAll for a batch of health checks

invokeAll waits for every task and keeps the input order. state(), resultNow() and exceptionNow() read finished futures without the checked ExecutionException.

Example · java
void main() throws InterruptedException {
    List<Callable<String>> checks = List.of(
        () -> "database: up",
        () -> "cache: up",
        () -> { throw new IllegalStateException("search: timed out"); });
    try (ExecutorService pool = Executors.newFixedThreadPool(2)) {
        for (Future<String> f : pool.invokeAll(checks)) {
            IO.println(switch (f.state()) {
                case SUCCESS -> f.resultNow();
                case FAILED -> "failed, " + f.exceptionNow().getMessage();
                default -> f.state().toString();
            });
        }
    }
}

// Output:
// database: up
// cache: up
// failed, search: timed out

Timing out and cancelling a slow task

Without cancel(true) the task would hold the pool's only thread for a full minute. Interrupting it frees the thread almost immediately.

Example · java
void main() throws Exception {
    try (ExecutorService pool = Executors.newSingleThreadExecutor()) {
        Future<String> report = pool.submit(() -> {
            Thread.sleep(Duration.ofMinutes(1));   // a downstream call that hangs
            return "report ready";
        });
        try {
            IO.println(report.get(200, TimeUnit.MILLISECONDS));
        } catch (TimeoutException e) {
            IO.println("gave up after 200 ms");
            IO.println("cancelled: " + report.cancel(true));   // interrupts the sleeping task
        }
        IO.println("state: " + report.state());
    }
}

// Output:
// gave up after 200 ms
// cancelled: true
// state: CANCELLED

A bounded pool that rejects overload

The first job goes straight to the new worker thread, the second waits in the queue and the third is rejected. Failing fast like this is better than letting an unbounded queue grow during an outage.

Example · java
void main() throws InterruptedException {
    CountDownLatch release = new CountDownLatch(1);
    ThreadPoolExecutor pool = new ThreadPoolExecutor(
        1, 1, 0, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1),             // at most one job waiting
        new ThreadPoolExecutor.AbortPolicy());   // reject anything beyond that
    for (int job = 1; job <= 3; job++) {
        try {
            pool.execute(() -> {
                try {
                    release.await();             // hold the worker busy
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            IO.println("job " + job + " accepted, queued: " + pool.getQueue().size());
        } catch (RejectedExecutionException e) {
            IO.println("job " + job + " rejected, answer 503");
        }
    }
    release.countDown();
    pool.shutdown();
    IO.println("finished: " + pool.awaitTermination(5, TimeUnit.SECONDS));
}

// Output:
// job 1 accepted, queued: 0
// job 2 accepted, queued: 1
// job 3 rejected, answer 503
// finished: true

Discussion

  • Be the first to comment on this lesson.