Что такое ExecutorService в Java и как им правильно пользоваться?

Ответ

ExecutorService — это интерфейс фреймворка java.util.concurrent, представляющий асинхронный механизм выполнения задач в пуле потоков. Он абстрагирует ручное создание и управление потоками (Thread), предоставляя высокоуровневый API для управления жизненным циклом задач.

Ключевые методы:

  • execute(Runnable task): Запускает задачу без возможности получить результат.
  • submit(Callable<T> task) / submit(Runnable task, T result): Отправляет задачу на выполнение и возвращает Future<T> для отслеживания статуса и получения результата.
  • invokeAll(Collection<Callable<T>> tasks): Запускает все задачи и возвращает список Future объектов, когда все завершатся.
  • invokeAny(Collection<Callable<T>> tasks): Запускает все задачи и возвращает результат первой успешно завершившейся.
  • shutdown(): Инициирует упорядоченное завершение, при котором выполняются ранее отправленные задачи, но новые задачи не принимаются.
  • shutdownNow(): Пытается остановить все активно выполняемые задачи, останавливает обработку ожидающих задач и возвращает список задач, которые не были выполнены.
  • awaitTermination(long timeout, TimeUnit unit): Блокирует поток до завершения всех задач после вызова shutdown() или истечения таймаута.

Создание пулов через Executors (фабричные методы):

// 1. Фиксированный пул потоков
ExecutorService fixedPool = Executors.newFixedThreadPool(10); // 10 рабочих потоков

// 2. Пул с кэшированием потоков (создает новые при необходимости)
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 3. Один поток (последовательное выполнение)
ExecutorService singleThread = Executors.newSingleThreadExecutor();

// 4. Пул для планирования задач
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(4);
scheduledPool.schedule(() -> System.out.println("Запуск через 5 сек"), 5, TimeUnit.SECONDS);
scheduledPool.scheduleAtFixedRate(() -> System.out.println("Повтор каждые 2 сек"), 1, 2, TimeUnit.SECONDS);

Правильное использование и завершение (шаблон try-with-resources с Java 19+):

// Рекомендуемый способ с Java 19+ (AutoCloseable)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { // или любой другой пул
    Future<Integer> future = executor.submit(() -> {
        // Длительная задача
        Thread.sleep(1000);
        return 42;
    });

    // Блокирующее получение результата с таймаутом
    Integer result = future.get(2, TimeUnit.SECONDS);
    System.out.println("Результат: " + result);
} // executor.shutdown() вызывается автоматически
catch (TimeoutException e) {
    System.err.println("Задача не завершилась вовремя");
}
catch (Exception e) {
    e.printStackTrace();
}

// Классический способ (до Java 19)
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
    // Отправка задач...
    List<Future<String>> futures = executor.invokeAll(listOfCallables);
    for (Future<String> f : futures) {
        System.out.println(f.get());
    }
} finally {
    executor.shutdown(); // Начинаем завершение
    try {
        // Ждем завершения всех задач, но не более 1 часа
        if (!executor.awaitTermination(1, TimeUnit.HOURS)) {
            executor.shutdownNow(); // Принудительная остановка
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt(); // Восстанавливаем флаг прерывания
    }
}

Тонкая настройка через ThreadPoolExecutor:

// Позволяет точно контролировать параметры пула
int corePoolSize = 5;    // Минимальное количество потоков
int maxPoolSize = 20;    // Максимальное количество потоков
long keepAliveTime = 60L; // Время простоя лишних потоков
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // Очередь задач
ThreadFactory threadFactory = Executors.defaultThreadFactory();
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // Политика при переполнении

ExecutorService customPool = new ThreadPoolExecutor(
    corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler
);

Политики обработки переполнения очереди (RejectedExecutionHandler):

  • AbortPolicy (по умолчанию): Выбрасывает RejectedExecutionException.
  • CallerRunsPolicy: Выполняет задачу в потоке, который вызвал execute/submit.
  • DiscardPolicy: Молча отбрасывает задачу.
  • DiscardOldestPolicy: Удаляет самую старую задачу из очереди и пытается добавить новую.

Лучшие практики:

  1. Всегда завершайте ExecutorService с помощью shutdown()/awaitTermination().
  2. Используйте Callable и Future для получения результатов и обработки исключений.
  3. Настройте размер пула в зависимости от типа задач (CPU-bound vs I/O-bound).
  4. Обрабатывайте RejectedExecutionException при высокой нагрузке.
  5. Рассмотрите CompletableFuture (Java 8+) для более сложных асинхронных цепочек и комбинаций.

Ответ 18+ 🔞

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

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

Вот смотри, какие у прораба кнопки есть:

  • execute(Runnable task) — это типа: «Вася, иди подмети двор». Послал и забыл, хуй узнаешь, подмёл он или нет.
  • submit(Callable<T> task) — а это уже: «Вася, сходи в магазин, купи водки и чек принеси». Ты ему даёшь Future — это как расписка, что водка будет. Потом по этой расписке можешь прийти и спросить: «Ну что, Вася, водка-то есть?» Или даже встать над душой с таймаутом: «Давай, блядь, за две секунды чек показывай!».
  • invokeAll(...) — это когда посылаешь сразу бригаду Васей на разные дела и стоишь, пока ВСЕ не отчитаются. Унылое ожидание, пиздец.
  • invokeAny(...) — а это хитро, ёпта! Послал десять Васей в десять магазинов за водкой. Кто первый принёс — того и чек принимаешь, остальных посылаешь нахуй, пусть водку себе оставляют.

А чтобы прораба нанять, есть контора готовая — Executors. Там тебе на выбор:

// 1. Бригада фиксированная. 10 человек, ни больше, ни меньше.
ExecutorService fixedPool = Executors.newFixedThreadPool(10);

// 2. Бригада временщиков. Задача есть — наняли человека. Задачи нет — «спасибо, свободен».
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 3. Один универсальный солдат. Всё делает по очереди, зато без гонки.
ExecutorService singleThread = Executors.newSingleThreadExecutor();

// 4. Бригада с будильником. «Через 5 секунд начинаем», «каждые 2 секунды долбим».
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(4);
scheduledPool.schedule(() -> System.out.println("Запуск через 5 сек"), 5, TimeUnit.SECONDS);

А теперь, самое важное, блядь! Этого прораба НАДО УВОЛЬНЯТЬ, когда работа закончена. А то он потоки держит, как дурак, и память жрёт!

Сейчас (Java 19+) модно так делать — он сам закроется:

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<Integer> future = executor.submit(() -> {
        Thread.sleep(1000); // Симулируем работу
        return 42; // Ответ на главный вопрос
    });
    Integer result = future.get(2, TimeUnit.SECONDS); // Ждём, но не долго, блядь
    System.out.println("Результат: " + result);
} catch (TimeoutException e) {
    System.err.println("Задача зависла, пошла нахуй.");
}

А если ты старовер, то вот тебе классический обряд завершения, чтоб ни один поток не остался висеть:

ExecutorService executor = Executors.newFixedThreadPool(4);
try {
    // Кидаешь задачи...
} finally {
    executor.shutdown(); // Говоришь: «Ребят, новых дел не даю, доделывайте что есть».
    try {
        // Ждёшь час, пока доделают
        if (!executor.awaitTermination(1, TimeUnit.HOURS)) {
            executor.shutdownNow(); // Не доделали? ВСЕМ УВОЛЬНЕНИЕ, НАХУЙ!
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt(); // Восстанавливаешь статус, а то затрёшь
    }
}

Если же ты совсем технарь и хочешь всё по полочкам, можешь собрать своего мега-прораба — ThreadPoolExecutor:

// Тут ты сам решаешь, сколько Васей в бригаде, какую им очередь задач дать и что делать, если все сдохли.
int corePoolSize = 5;    // Постоянный костяк
int maxPoolSize = 20;    // Максимум наёмников в аврал
long keepAliveTime = 60L; // Сколько лишние наёмники ждут работы, прежде чем сваливать
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100); // Очередь из 100 поручений
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); // Что делать при полной очереди

ExecutorService customPool = new ThreadPoolExecutor(
    corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, handler
);

Про политики при переполнении (RejectedExecutionHandler): Это что делать, когда и очередь из 100 задач забита, и все 20 Васей заняты, а ты пытаешься запихнуть 101-ю задачу.

  • AbortPolicy (стандартная): Выбросит тебе в рожу RejectedExecutionException. «Сам дурак, блядь».
  • CallerRunsPolicy (по-человечески): Задачу выполнит тот поток, который её пытался запихнуть. То есть ты сам, царь, идёшь пол мыть. Справедливо, ёпта.
  • DiscardPolicy (по-тихому): Просто выкидывает задачу в никуда. «Не было — и хуй с ним».
  • DiscardOldestPolicy (беспринципно): Выкидывает самую старую задачу из очереди и пихает на её место новую. «Извини, старик, твоё время вышло».

Итоговые мудрости, блядь:

  1. ЗАКРЫВАЙ пулы. Это не мусорка, чтоб их не чистить.
  2. Используй submit() с Future, если тебе важен результат или исключение. Иначе потом будешь гадать, почему всё упало, а в логах нихуя.
  3. Размер пула — это святое. Для задач, где CPU пашет (типа вычислений) — пул размером с количество ядер. Для задач, где ждёшь ответа от сети/БД (I/O-bound) — можно больше, чтоб не простаивать.
  4. Не игнорируй RejectedExecutionException. Это крик системы: «Ёбаный насос, я не справляюсь!».
  5. Для сложных сценариев, где одна задача запускает другую, глянь в сторону CompletableFuture. Это уже высший пилотаж, но и пиздеца там может быть овердохуища.

Вот и вся магия, сука. Не так страшен чёрт, как его малюют. Главное — понять, что это просто надстройка, чтобы твои потоки не болтались без дела как пизда в проруби.