JavaRush /Курсы /JAVA 25 SELF /ExecutorService, Callable, Future: запуск задач

ExecutorService, Callable, Future: запуск задач

JAVA 25 SELF
54 уровень , 1 лекция
Открыта

1. ExecutorService: управляем потоками как взрослые

Почему не стоит просто создавать потоки через new Thread

На первых порах многопоточности всё выглядит просто:

Thread t = new Thread(() -> {
    // что-то делаем
});
t.start();

Такой способ работает, но быстро становится обузой, когда задач становится много. Каждый вызов new Thread() создаёт новый поток, а десятки или сотни потоков начинают перегружать систему. К тому же управлять ими неудобно: нужно следить, когда они заканчиваются, что делать при ошибках, как останавливать и переиспользовать их.

Вот здесь на сцену выходит ExecutorService — умный диспетчер потоков. Вы просто отдаёте ему задачи, а он сам решает, каким потоком и когда их выполнить. В результате всё работает быстрее, стабильнее и без головной боли.

Как устроен ExecutorService

ExecutorService работает по простому, но эффективному принципу.

  • Внутри него есть пул потоков — заранее созданный набор рабочих потоков (фиксированный или динамический).
  • Задачи попадают в очередь и подхватываются свободными потоками.
  • Сервис управляет жизненным циклом: вы можете дождаться завершения, корректно остановить пул и освободить ресурсы.

Создание ExecutorService

Самый частый способ — использовать фабричные методы из класса Executors:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(4); // 4 потока
  • newFixedThreadPool(N) — пул из N потоков (подходит для большинства задач).
  • newCachedThreadPool() — динамический пул, создаёт потоки по необходимости (осторожно: можно «упереться» в память при лавине задач).
  • newSingleThreadExecutor() — один поток (последовательное выполнение).

Пример: запуск Runnable через ExecutorService

executor.submit(() -> {
    System.out.println("Привет из пула потоков!");
});

После того как вы закончили работу с ExecutorService, его нужно корректно завершить:

executor.shutdown(); // Запрещает добавлять новые задачи, ждёт завершения текущих

Важно: Если не вызвать shutdown(), программа может не завершиться — потоки из пула будут ждать новых задач.

2. Runnable vs Callable: задачи бывают разные

До Java 5, если вы хотели что-то выполнить в потоке, вы писали реализацию интерфейса Runnable. Это задача, которая ничего не возвращает и не бросает проверяемых исключений.

Runnable task = () -> {
    System.out.println("Просто работаю, ничего не возвращаю!");
};
executor.submit(task);

Callable: задача с результатом (и исключениями)

Иногда хочется, чтобы задача не просто «что-то делала», а возвращала результат — например, сумму чисел, результат вычислений, данные с сервера. Для этого придумали интерфейс Callable<T>.

import java.util.concurrent.Callable;

Callable<Integer> sumTask = () -> {
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
};
  • Метод call() возвращает результат типа T.
  • Метод call() может бросать проверяемое исключение.

Аналогия: Runnable — «иди и помой посуду» (результат не важен), Callable — «иди и принеси чай и скажи, какой он температуры» (результат важен).

Запуск Callable: чтобы получить результат, используйте executor.submit(...). Он вернёт объект Future<T>.

3. Future: обещание результата

Future — это «обещание» вернуть результат в будущем. Когда вы отправляете задачу в ExecutorService, вы получаете Future, из которого позже сможете получить результат, узнать, завершилась ли задача, или отменить её.

Основные методы Future

  • T get() — получить результат (ждёт, пока задача завершится).
  • boolean isDone() — завершена ли задача.
  • boolean cancel(boolean mayInterruptIfRunning) — попытаться отменить задачу.
  • boolean isCancelled() — была ли задача отменена.

Пример: запуск Callable и получение результата

import java.util.concurrent.*;

public class ParallelSumApp {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Callable<Integer> sumTask = () -> {
            int sum = 0;
            for (int i = 1; i <= 100; i++) sum += i;
            return sum;
        };

        Future<Integer> future = executor.submit(sumTask);

        System.out.println("Задача запущена, можно делать что-то ещё...");

        // Получаем результат (метод блокирует поток, если задача ещё не завершена)
        Integer result = future.get();
        System.out.println("Результат вычислений: " + result);

        executor.shutdown();
    }
}
  • Задача отправляется в пул потоков.
  • Пока задача выполняется, основной поток может делать что-то ещё.
  • Когда результат нужен, вызываем future.get() — поток подождёт, если задача ещё в работе.
  • Как только задача завершится, результат вернётся.

4. Практика: несколько задач, ожидание завершения

Часто нужно запустить сразу несколько задач и дождаться, когда они все завершатся. Например, вы обрабатываете массив данных, разбиваете его на части и считаете сумму каждой части в отдельной задаче.

Пример: сумма элементов массива частями

import java.util.*;
import java.util.concurrent.*;

public class ParallelArraySum {
    public static void main(String[] args) throws Exception {
        int[] array = new int[1000];
        Arrays.setAll(array, i -> i + 1); // Заполняем числами от 1 до 1000

        ExecutorService executor = Executors.newFixedThreadPool(4);

        int chunkSize = array.length / 4;
        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 0; i < 4; i++) {
            int from = i * chunkSize;
            int to = (i == 3) ? array.length : (i + 1) * chunkSize;

            Callable<Integer> sumTask = () -> {
                int sum = 0;
                for (int j = from; j < to; j++) sum += array[j];
                System.out.println("Сумма от " + from + " до " + (to - 1) + " = " + sum);
                return sum;
            };

            futures.add(executor.submit(sumTask));
        }

        int totalSum = 0;
        for (Future<Integer> f : futures) {
            totalSum += f.get(); // Ждём каждую задачу по очереди
        }

        System.out.println("Общая сумма: " + totalSum);

        executor.shutdown();
    }
}

Здесь массив разбивается на 4 части. Для каждой части создаётся задача (Callable), которая считает сумму. Все задачи отправляются в ExecutorService, возвращаются Future. В конце собираем результаты всех задач и складываем.

В реальных задачах удобно использовать invokeAll, чтобы дождаться выполнения всех задач сразу.

5. Обработка ошибок при работе с Future

Когда вы вызываете future.get(), если задача завершилась с исключением, оно будет проброшено как ExecutionException. Это важно: если в задаче что-то пошло не так, вы об этом узнаете только при вызове get().

Пример: обработка исключений

Callable<Integer> errorTask = () -> {
    throw new IllegalArgumentException("Что-то пошло не так!");
};

Future<Integer> badFuture = executor.submit(errorTask);

try {
    badFuture.get();
} catch (ExecutionException e) {
    System.out.println("Задача завершилась с ошибкой: " + e.getCause());
}
  • Внутри задачи выбрасывается исключение.
  • При вызове get() оно «заворачивается» в ExecutionException.
  • Настоящую причину можно получить через getCause().

6. Полезные нюансы

Как отменить задачу

Future<?> f = executor.submit(() -> {
    while (true) {
        // Бесконечная работа
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("Меня попросили завершиться!");
            break;
        }
    }
});

Thread.sleep(100); // Подождём чуть-чуть
f.cancel(true); // Попробуем отменить задачу
  • cancel(true) пытается прервать задачу, если она ещё не завершена.
  • Внутри задачи желательно проверять Thread.currentThread().isInterrupted() и корректно завершаться.

shutdown vs shutdownNow

shutdown() — мягкая остановка: запрещает добавлять новые задачи и даёт текущим спокойно завершиться. Используется чаще всего.

shutdownNow() — жёсткая остановка: пытается прервать активные потоки и возвращает список задач, которые не успели стартовать. Применяйте осторожно.

invokeAll и invokeAny

invokeAll(Collection<Callable<T>> tasks) запускает все переданные задачи и ждёт, пока они все завершатся. Возвращает список Future.

invokeAny(Collection<Callable<T>> tasks) ждёт только первую успешно выполненную задачу, возвращает её результат и отменяет остальные. Удобно, когда важен первый успешный ответ.

7. Типичные ошибки при работе с ExecutorService, Callable и Future

Ошибка №1: Не закрывать ExecutorService. Если забыть вызвать shutdown(), программа может «висеть» после завершения main, потому что потоки пула ждут новых задач.

Ошибка №2: Ожидание результата сразу после отправки задачи. Если сразу после submit() вызвать get(), вы не получите преимуществ асинхронности — поток всё равно будет ждать. Делайте полезную работу параллельно и запрашивайте результат, когда он действительно нужен.

Ошибка №3: Игнорирование исключений в задачах. Если не обрабатывать ExecutionException при вызове get(), можно пропустить важные ошибки, которые произошли в задаче.

Ошибка №4: Использование общих изменяемых переменных без синхронизации. Если несколько задач работают с одними и теми же данными — нужна синхронизация или потокобезопасные коллекции.

Ошибка №5: Создание слишком большого количества потоков. Не стоит делать пул с числом потоков, сильно превышающим количество ядер процессора — это может даже замедлить выполнение.

Ошибка №6: Забывать про отмену задач. Если задача больше не нужна, отменяйте её через cancel(), чтобы не тратить ресурсы.

1
Задача
JAVA 25 SELF, 54 уровень, 1 лекция
Недоступна
Отправка срочного пакета на дроне 📦
Отправка срочного пакета на дроне 📦
1
Задача
JAVA 25 SELF, 54 уровень, 1 лекция
Недоступна
Расшифровка древнего предсказания 🔮
Расшифровка древнего предсказания 🔮
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ