Thread pools and ExecutorService
Creating a thread by hand for every task is clumsy and costly: a thread takes memory, and starting one takes time. Tasks are usually handed to a thread pool — a set of threads created in advance that take tasks one after another and run them. In Java a pool is represented by the ExecutorService interface, and the Executors class creates ready-made pools.
The first task
The submit method sends a task to the pool and immediately returns a Future — a "promise" of a result that will appear later. The get() method waits until the task finishes and returns the result:
void main() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(() -> 6 * 7);
IO.println("Result: " + future.get());
executor.shutdown();
}Output:
Result: 42newFixedThreadPool(2) creates a pool of exactly two threads. A lambda that returns a value is a Callable, not a Runnable. The shutdown() method tells the pool: there will be no new tasks, finish after the current ones.
Closing with try-with-resources
Since Java 19 ExecutorService implements AutoCloseable. If you create the pool in try-with-resources, at the end of the block it waits for all tasks and closes by itself:
void main() throws Exception {
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
Future<String> greeting = executor.submit(() -> "Hello, Anna");
Future<Integer> length = executor.submit(() -> "Hello, Anna".length());
IO.println(greeting.get());
IO.println(length.get());
}
IO.println("Pool closed");
}Output:
Hello, Anna
11
Pool closedThe tasks ran in parallel on two threads, but the output is predictable: we print the results ourselves, in the order we want, after get().
Many tasks at once
invokeAll takes a list of tasks, waits for all of them and returns a list of Future objects in the same order:
int slowSquare(int n) throws InterruptedException {
Thread.sleep(100);
return n * n;
}
void main() throws Exception {
List<Callable<Integer>> tasks = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
int n = i;
tasks.add(() -> slowSquare(n));
}
try (ExecutorService executor = Executors.newFixedThreadPool(5)) {
List<Integer> results = new ArrayList<>();
for (Future<Integer> f : executor.invokeAll(tasks)) {
results.add(f.get());
}
IO.println(results);
}
}Output:
[1, 4, 9, 16, 25]Five tasks of 100 milliseconds each ran at the same time, so the whole thing took about 100 ms rather than half a second. The variable n is needed because a lambda can only use variables that don't change, and i changes in the loop.
Errors in tasks
If a task throws an exception, it isn't lost: get() throws an ExecutionException, and the original error is available through getCause():
void main() throws InterruptedException {
try (ExecutorService executor = Executors.newFixedThreadPool(1)) {
Future<Integer> future = executor.submit(() -> Integer.parseInt("forty"));
try {
IO.println(future.get());
} catch (ExecutionException e) {
IO.println("The task failed: " + e.getCause());
}
}
}Output:
The task failed: java.lang.NumberFormatException: For input string: "forty"Timeout
The get method can be limited in time. If the result isn't ready in time, a TimeoutException is thrown, and the task can be cancelled with cancel:
void main() throws Exception {
try (ExecutorService executor = Executors.newFixedThreadPool(1)) {
Future<String> future = executor.submit(() -> {
Thread.sleep(5000);
return "Server reply";
});
try {
IO.println(future.get(200, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
future.cancel(true);
IO.println("The server didn't answer within 200 ms");
}
}
}Output:
The server didn't answer within 200 mscancel(true) interrupts the task's thread: Thread.sleep throws InterruptedException, the task ends, and the pool can close without waiting five seconds.
Common mistake
If you create a pool without try-with-resources and forget to call shutdown(), the program won't end: after main finishes, the pool's threads keep waiting for new tasks. It looks like a hang — everything is printed, yet the process keeps running.
void main() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
IO.println(executor.submit(() -> "task done").get());
} finally {
executor.shutdown();
}
}Output:
task doneClose the pool in finally or, simpler still, create it in try-with-resources — then you can't forget to close it.