Пулы потоков и 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();
}Результат:
Результат: 42newFixedThreadPool(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 — тогда забыть о закрытии невозможно.