Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Пулы потоков и ExecutorService

Создавать поток на каждую задачу вручную неудобно и дорого: поток занимает память, а его запуск требует времени. Обычно задачи отдают пулу потоков — набору заранее созданных потоков, которые по очереди берут задачи и выполняют их. В Java пул представлен интерфейсом ExecutorService, а готовые пулы создаёт класс Executors.

Первая задача

Метод submit отправляет задачу в пул и сразу возвращает Future — «обещание» результата, который появится позже. Метод get() ждёт, пока задача выполнится, и возвращает результат:

void main() throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(2);
 
    Future<Integer> future = executor.submit(() -> 6 * 7);
    IO.println("Результат: " + future.get());
 
    executor.shutdown();
}

Результат:

Результат: 42

newFixedThreadPool(2) создаёт пул ровно из двух потоков. Лямбда, которая возвращает значение, — это Callable, а не Runnable. Метод shutdown() говорит пулу: новых задач не будет, можно завершаться после текущих.

Закрытие через try-with-resources

С Java 19 ExecutorService реализует AutoCloseable. Если создать пул в try-with-resources, в конце блока он сам дождётся всех задач и закроется:

void main() throws Exception {
    try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
        Future<String> greeting = executor.submit(() -> "Привет, Анна");
        Future<Integer> length = executor.submit(() -> "Привет, Анна".length());
 
        IO.println(greeting.get());
        IO.println(length.get());
    }
    IO.println("Пул закрыт");
}

Результат:

Привет, Анна
12
Пул закрыт

Задачи выполнялись параллельно в двух потоках, но вывод предсказуем: мы печатаем результаты сами, в нужном порядке, после get().

Много задач сразу

invokeAll принимает список задач, ждёт выполнения всех и возвращает список Future в том же порядке:

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);
    }
}

Результат:

[1, 4, 9, 16, 25]

Пять задач по 100 миллисекунд выполнились одновременно, поэтому всё заняло около 100 мс, а не полсекунды. Переменная n нужна потому, что лямбда может использовать только неизменяемые переменные, а i меняется в цикле.

Ошибки в задачах

Если задача выбросила исключение, оно не теряется: get() выбросит ExecutionException, а исходную ошибку можно получить через getCause():

void main() throws InterruptedException {
    try (ExecutorService executor = Executors.newFixedThreadPool(1)) {
        Future<Integer> future = executor.submit(() -> Integer.parseInt("сорок"));
        try {
            IO.println(future.get());
        } catch (ExecutionException e) {
            IO.println("Задача упала: " + e.getCause());
        }
    }
}

Результат:

Задача упала: java.lang.NumberFormatException: For input string: "сорок"

Тайм-аут

Метод get можно ограничить по времени. Если результат не готов вовремя, выбрасывается TimeoutException, а задачу можно отменить методом cancel:

void main() throws Exception {
    try (ExecutorService executor = Executors.newFixedThreadPool(1)) {
        Future<String> future = executor.submit(() -> {
            Thread.sleep(5000);
            return "Ответ сервера";
        });
 
        try {
            IO.println(future.get(200, TimeUnit.MILLISECONDS));
        } catch (TimeoutException e) {
            future.cancel(true);
            IO.println("Сервер не ответил за 200 мс");
        }
    }
}

Результат:

Сервер не ответил за 200 мс

cancel(true) прерывает поток задачи: Thread.sleep выбрасывает InterruptedException, задача завершается, и пул может закрыться, не дожидаясь пяти секунд.

Частая ошибка

Если создать пул без try-with-resources и забыть вызвать shutdown(), программа не завершится: после выполнения main потоки пула продолжают ждать новых задач. Внешне это выглядит как зависание — всё напечатано, а процесс работает.

void main() throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(2);
    try {
        IO.println(executor.submit(() -> "задача выполнена").get());
    } finally {
        executor.shutdown();
    }
}

Результат:

задача выполнена

Закрывайте пул в finally или, проще, создавайте его в try-with-resources — тогда забыть о закрытии невозможно.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.