Питання 14 · Розділ 9

Що робить ExecutorService?

ExecutorService — це основний інтерфейс в Java для управління пулом потоків та виконання асинхронних задач.

Мовні версії: English Russian Ukrainian

Junior рівень

Базове розуміння

ExecutorService — це основний інтерфейс в Java для управління пулом потоків та виконання асинхронних задач.

Без ExecutorService: ви вручну створюєте потоки (new Thread().start()), контролюєте їх життєвий цикл та обробляєте помилки. З ExecutorService: ви лише надсилаєте задачі та отримуєте результати — управління потоками бере на себе фреймворк. Він відокремлює “що робити” (задачу) від “як і коли робити” (механізм виконання).

Простий приклад

// Створення ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(3);

// Надсилання задачі
executor.submit(() -> {
    System.out.println("Задачу виконано!");
});

// Завершення
executor.shutdown();

Основні способи надсилання задач

Метод Повертає Винятки Коли використовувати
execute(Runnable) void Вилітають у консоль “Вистрілив і забув”
submit(Runnable) Future<?> Упаковані в ExecutionException — обгортка потрібна, щоб відрізнити помилку задачі від помилки самого ExecutorService. Оригінальний виняток доступний через e.getCause(). Потрібно відстежити завершення
submit(Callable<T>) Future Упаковані в ExecutionException — обгортка потрібна, щоб відрізнити помилку задачі від помилки самого ExecutorService. Оригінальний виняток доступний через e.getCause(). Потрібен результат

execute() vs submit()

// execute() — "вистрілив і забув"
executor.execute(() -> {
    System.out.println("Виконуюсь...");
    // Помилка тут — просто надрукується в System.err
});

// submit() — можна отримати результат і помилку
Future<?> future = executor.submit(() -> {
    System.out.println("Виконуюсь...");
    // Помилка тут — буде в future.get()
});

// Перевірка завершення
if (future.isDone()) {
    System.out.println("Задачу завершено");
}

Закриття пулу

// Спосіб 1: Просте завершення
executor.shutdown(); // Перестаємо приймати нові задачі
executor.awaitTermination(60, TimeUnit.SECONDS); // Чекаємо до 60 секунд

// Спосіб 2: Примусове завершення
executor.shutdownNow(); // Перериває активні задачі

Middle рівень

Життєвий цикл ExecutorService

┌──────────┐    shutdown()    ┌───────────┐  всі задачі  ┌─────────┐
│ RUNNING  │ ──────────────→ │ SHUTDOWN  │ ────────────→ │ TIDYING │
│          │                 │           │               │         │
│ при-     │  shutdownNow()  │ не при-   │               │         │
| ймає     │ ──────────────→ | ймає      │               │         │
│ задачі   │                 │ задачі    │               │         │
│          │  ←───────────── │ доробляє  │               │         │
│ оброб-   │   interrupted    │ чергу     │               │         │
│ ляє      │                 │           │               │         │
└──────────┘                 └───────────┘               └────┬────┘
      │                                                       │
      │                                                       │ terminated()
      │                                                       ▼
      │                                                 ┌───────────┐
      └────────────────────────────────────────────────│ TERMINATED│
                                                       └───────────┘
Стан Опис
RUNNING Приймає та обробляє задачі
SHUTDOWN Не приймає нові, доробляє чергу
STOP Не приймає, перериває активні
TIDYING Всі задачі завершено, потоки зупинено
TERMINATED terminated() виконано

submit(): Future та обробка помилок

// Callable — повертає результат
Future<String> future = executor.submit(() -> {
    return "Результат";
});

// Отримання результату (блокує!)
String result = future.get();

// Отримання з таймаутом
try {
    String result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
    future.cancel(true); // Скасовуємо задачу
}
// cancel(true) посилає задачі interrupt().
// Задача має сама перевірити Thread.interrupted() і завершитись.
// Якщо задача не обробляє переривання — вона продовжить працювати!

invokeAll() та invokeAny()

// invokeAll() — чекає ВСІ задачі
List<Callable<String>> tasks = List.of(
    () -> "Task 1",
    () -> "Task 2",
    () -> "Task 3"
);
List<Future<String>> futures = executor.invokeAll(tasks);
// Блокує, поки всі не завершаться

// invokeAny() — чекає ПЕРШУ успішну задачу
String result = executor.invokeAny(tasks);
// Повертає результат першої завершеної задачі, решту скасовує

Правильне завершення пулу

public void shutdownGracefully(ExecutorService executor) {
    executor.shutdown(); // 1. Перестаємо приймати
    try {
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 2. Чекаємо
            executor.shutdownNow(); // 3. Примусово
            if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                System.err.println("Pool did not terminate");
            }
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt(); // Відновлюємо флаг
    }
}

submit() vs execute() — пастка з винятками

// execute() — виняток вилітає одразу
executor.execute(() -> {
    throw new RuntimeException("Помилка!"); // → UncaughtExceptionHandler
});

// submit() — виняток "схований" у Future
Future<?> future = executor.submit(() -> {
    throw new RuntimeException("Помилка!");
});
// Жодної помилки поки ви не викличете future.get()!
try {
    future.get(); // → ExecutionException
} catch (ExecutionException e) {
    System.out.println(e.getCause()); // → RuntimeException: Помилка!
}

Senior рівень

Деталі реалізації ThreadPoolExecutor (ctl, черги, ThreadFactory) — у файлі [[12. Що таке пул потоків (Thread Pool)]].

Thread Pool Leaks

// ПОГАНО: створення пулу всередині методу
public void process() {
    ExecutorService executor = Executors.newFixedThreadPool(10);
    executor.submit(() -> doWork());
    executor.shutdown();
    // Якщо doWork() кидає виняток — shutdown() не викликається
    // Потоки витікають!
}

// ГАРНО: пул — синглтон або управляється контейнером
@Component
public class TaskProcessor {
    private final ExecutorService executor;

    public TaskProcessor() {
        this.executor = new ThreadPoolExecutor(...);
    }

    @PreDestroy
    public void shutdown() {
        shutdownGracefully(executor);
    }
}

Correlation ID / Trace Context

// Проблема: Trace ID не пробрасується автоматично
executor.submit(() -> {
    // MDC.get("traceId") = null!
});

// Рішення: обгортка
public class TracingExecutorService implements ExecutorService {
    private final ExecutorService delegate;

    public Future<?> submit(Runnable task) {
        Map<String, String> mdcContext = MDC.getCopyOfContextMap();
        return delegate.submit(() -> {
            if (mdcContext != null) MDC.setContextMap(mdcContext);
            try {
                task.run();
            } finally {
                MDC.clear();
            }
        });
    }
}

Діагностика

Метрики для моніторингу

// Виводимо в Prometheus:
executor.getActiveCount();         // Активних потоків
executor.getQueue().size();        // Розмір черги
executor.getCompletedTaskCount();  // Завершених задач
executor.getTaskCount();           | Всього задач
executor.getPoolSize();            // Розмір пулу

Future.isDone() / isCancelled()

Future<?> future = executor.submit(task);

// Без блокування
if (future.isDone()) {
    System.out.println("Завершена");
}
if (future.isCancelled()) {
    System.out.println("Скасована");
}

Best Practices

  1. Не створюйте ExecutorService всередині методу — витік потоків
  2. Використовуйте submit() — якщо потрібна обробка помилок
  3. Завжди викликайте shutdown() — у finally або @PreDestroy
  4. ArrayBlockingQueue — захист від OOM
  5. CallerRunsPolicy — back-pressure при перевантаженні
  6. Кастомний ThreadFactory — іменування потоків
  7. Trace ID propagation — для мікросервісів
  8. Моніторте queue.size() — черга, що росте = деградація

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • ExecutorService — інтерфейс управління пулом потоків: надсилання задач, отримання результатів, graceful shutdown
  • execute(Runnable) = void, винятки вилітають у консоль; submit() = Future, винятки сховані в ExecutionException
  • submit(Callable) повертає Future — можна отримати результат та обробити помилку через get()
  • Життєвий цикл: RUNNING → SHUTDOWN (shutdown()) → STOP (shutdownNow()) → TIDYING → TERMINATED
  • invokeAll() — чекає ВСІ задачі; invokeAny() — повертає результат ПЕРШОЇ успішної, решту скасовує
  • Graceful shutdown: shutdown() → awaitTermination(timeout) → shutdownNow() → awaitTermination
  • Future.get(timeout) кидає TimeoutException — при таймауті потрібно викликати future.cancel(true)
  • ExecutorService має управлятися контейнером (Spring @Bean) або синглтоном, НЕ створюватися всередині методу

Часті уточнюючі запитання:

  • У чому різниця execute() vs submit()? — execute «вистрілив і забув», submit дозволяє відстежити результат і помилку через Future
  • Чому submit() не кидає виняток одразу? — Виняток упакований в ExecutionException і доступний тільки через future.get().getCause()
  • Чим shutdown() відрізняється від shutdownNow()? — shutdown доробляє чергу, shutdownNow перериває активні задачі через interrupt()
  • Як пробросити MDC/Trace ID в ExecutorService? — Обгортка: MDC.getCopyOfContextMap() перед submit, MDC.setContextMap() всередині задачі

Червоні прапорці (НЕ говорити):

  • “Створюю ExecutorService всередині методу” — витік потоків, якщо shutdown() не викликається
  • “submit() одразу покаже виняток” — ні, потрібно викликати future.get()
  • “shutdownNow() гарантовано зупиняє всі задачі” — ні, лише посилає interrupt(), задачі мають самі обробити
  • “cancel(true) миттєво вбиває задачу” — ні, лише виставляє interrupted flag, задача має перевірити

Пов’язані теми:

  • [[12. Що таке пул потоків (Thread Pool)]]
  • [[13. Які типи Thread Pool існують в Java]]
  • [[16. В чому різниця між Executors.newFixedThreadPool() та newCachedThreadPool()]]
  • [[17. Що таке ForkJoinPool]]