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
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
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.
Other executor flavors
Executors.newFixedThreadPool(n)— a pool with exactlynreusable 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.
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 ofnworker threads..submit(task)runs a task asynchronously and returns aFutureyou 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.