JavaExecutor Framework

Executor Framework

Creating a raw Thread for every piece of work (as covered on the Threads page) is fine for a handful of tasks, but it does not scale. Every new Thread(...) you start costs memory for its call stack and real OS scheduling overhead, and if you spin up thousands of them for thousands of short-lived tasks, you will exhaust system resources long before you exhaust the actual work. The java.util.concurrent package (added in Java 5) solves this with the Executor Framework — a higher-level, managed way to run concurrent tasks without manually creating and tracking Thread objects yourself.

Thread pools: reuse instead of recreate

The central idea is a thread pool: a fixed set of worker threads that are created once and then reused to run many tasks over their lifetime, instead of paying the cost of creating and destroying a thread for every single task. You hand tasks to an ExecutorService, and it queues them up and hands them off to whichever worker thread is free next.

Creating and using a fixed thread pool

Java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        // A pool of exactly 4 reusable worker threads
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            executor.submit(() -> {
                System.out.println("Running task " + taskId
                        + " on " + Thread.currentThread().getName());
            });
        }

        // Always shut the pool down when you're done submitting work
        executor.shutdown();
    }
}

Even though 10 tasks were submitted, only 4 worker threads ever exist. Each thread simply picks up the next queued task once it finishes the previous one — far cheaper than creating 10 separate Thread objects.

submit() and Future

Unlike Thread.start(), which returns nothing, ExecutorService.submit() returns a Future — a handle representing the eventual result of the task. You can call .get() on it to block until the result is ready, check .isDone(), or cancel it with .cancel().

submit() returns a Future

Java
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Future<Integer> future = executor.submit(() -> {
            Thread.sleep(500);
            return 21 * 2;
        });

        System.out.println("Task submitted, doing other work...");

        int result = future.get(); // blocks until the task finishes
        System.out.println("Result: " + result);

        executor.shutdown();
    }
}

The next page, CompletableFuture, builds on exactly this idea but removes the need to block on .get() at all.

Warning
An `ExecutorService`'s worker threads are **non-daemon** by default, which means the JVM will not exit while any of them are still alive. If you forget to call `.shutdown()` (or `.shutdownNow()`), your program can hang indefinitely after `main()` appears to finish, because the pool's threads are still sitting there waiting for more work. Always shut down every executor you create, ideally in a `finally` block or with a try-with-resources style helper.
Other executor flavors
  • Executors.newFixedThreadPool(n) — a pool with exactly n reusable threads, good for CPU-bound or steady workloads.

  • Executors.newCachedThreadPool() — creates new threads as needed and reuses idle ones, good for many short-lived, bursty tasks.

  • Executors.newSingleThreadExecutor() — a pool of exactly one thread, useful when tasks must run strictly one at a time, in order.

  • Executors.newScheduledThreadPool(n) — supports running tasks after a delay or on a repeating schedule.

Java 21: virtual threads
Java 21 introduced **virtual threads** via `Executors.newVirtualThreadPerTaskExecutor()`, a major evolution in Java concurrency. Instead of a small, fixed pool of expensive OS threads, this executor creates a lightweight virtual thread for every task — cheap enough that you can comfortably run millions of them at once, which changes the practical rules of thumb around how much concurrency an application can afford. The details of how virtual threads work under the hood are a deeper topic on their own; for now, it is enough to know they exist as a drop-in `ExecutorService` you can reach for when you have huge numbers of blocking, I/O-bound tasks.
  • ExecutorService manages a pool of reusable worker threads instead of creating a new Thread per task.

  • Executors.newFixedThreadPool(n) is the standard way to create a pool of n worker threads.

  • .submit(task) runs a task asynchronously and returns a Future you can use to retrieve its result.

  • Always call .shutdown() when done — its threads are non-daemon and will keep the JVM alive otherwise.

  • Java 21's Executors.newVirtualThreadPerTaskExecutor() enables extremely lightweight, massively scaled concurrency.