Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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: 42

newFixedThreadPool(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 closed

The 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 ms

cancel(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 done

Close the pool in finally or, simpler still, create it in try-with-resources — then you can't forget to close it.

Thanks for reading!

Donate to the site — we work hard for you.