ExecutorService: как запускать потоки и не уронить прод
В прошлый раз мы выбрали правильные коллекции. Но сама по себе потокобезопасная структура мертва, пока её не начнут дергать потоки.
Типичная ошибка — породить тысячу тредов через `new Thread()`:
// ПЛОХОЙ КОД
for (int i = 0; i < 1000; i++) {
new Thread(() -> callApi(i)).start();
}
ОС просто убьет такой процесс из-за нехватки ресурсов или задушит переключениями контекста.
Как делаем мы
Используем пул потоков (ExecutorService). Мы описываем задачу, а пул сам решает, какой свободный воркер её заберет.
Базовый шаблон работы выглядит так:
/
/ Пул размером с количество ядер CPU
ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
// Запускаем задачи
for (int i = 0; i < 10_000; i++) {
int id = i;
// submit возвращает Future — ручку для отслеживания результата
executor.submit(() -> processTask(id));
}
// Мягко останавливаем прием новых задач
executor.shutdown();
try {
// Ждем завершения запущенных (максимум 2 минуты)
if (!executor.awaitTermination(2, TimeUnit.MINUTES)) {
System.err.println("Принудительная отмена");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Восстанавливаем флаг прерывания
executor.shutdownNow();
}
Три правила, чтобы спать спокойно:
* Исключения. Ошибки внутри обычного Runnable проглатываются. Если хотим их видеть — используем Callable<V> и вызываем future.get(). Там любое исключение вылетит наружу в виде ExecutionException.
* Размер пула. Для тяжелых вычислений (CPU-bound) ставим столько нитей, сколько физических ядер. Для ожидания сети или диска (IO-bound) — больше, например (ядра * 2), чтобы пока одни спят, другие работали.
* Shutdown vs ShutdownNow. Первый мягко закрывает входную дверь. Второй пытается прервать уже работающие нити через thread.interrupt(). Наш код обязан уметь обрабатывать InterruptedException и быстро завершаться.
Забудьте про ручной new Thread(). Любой фоновый чих должен идти через ExecutorService. Это дает контроль над памятью и позволяет приложению нормально закрыться.