How does the Executor framework in Java manage threads, and which factory methods matter in practice?
The Executor framework separates two concerns that used to be tangled together: creating threads and running tasks. You hand work to an ExecutorService, and it decides when and on which thread to run it, using a pool of reusable threads. Because the threads are reused, you avoid the cost of creating one per task, and because the pool is bounded, a burst of work cannot spawn an unbounded number of threads.
The factory methods you will actually use. newFixedThreadPool(n) gives you exactly n threads and queues everything else. newCachedThreadPool() gives you threads on demand and reclaims idle ones. newSingleThreadExecutor() serialises all the work onto one thread, which is how you get a sequential pipeline. And newVirtualThreadPerTaskExecutor() creates a fresh virtual thread per task.
There are two older variants worth knowing about. newSingleThreadExecutor has an unbounded queue, so a slow consumer causes the queue to grow without limit and the program runs out of memory rather than failing fast. newFixedThreadPool is also unbounded. The ThreadPoolExecutor constructor lets you supply your own queue and rejection policy, and that is what you want when backpressure matters.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) throws InterruptedException {
ExecutorService fixed = Executors.newFixedThreadPool(3);
ExecutorService single = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 5; i++) {
int task = i;
fixed.submit(() -> {
System.out.println("task " + task + " on " + Thread.currentThread().getName());
sleepQuietly(20);
});
}
single.submit(() -> System.out.println("single task on " + Thread.currentThread().getName()));
fixed.shutdown();
single.shutdown();
fixed.awaitTermination(2, TimeUnit.SECONDS);
single.awaitTermination(2, TimeUnit.SECONDS);
System.out.println("pools shut down");
}
static void sleepQuietly(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}The output shows the pools at work. The five tasks share the three threads of the fixed pool, so they run in batches, while the single-threaded executor runs its one task on its own thread.
The other half of the API is invokeAll and invokeAny, which submit a collection of Callables and wait for them. They are worth knowing because they return a list of Futures and handle the exception wrapping for you.