ExecutorService: как запускать задачи и не устроить DDoS самому себе


В прошлом посте мы разобрали потокобезопасные коллекции.
Но возникает следующий вопрос: кто вообще должен работать с этими коллекциями?
Можно, конечно, сделать так:
for (int i = 0; i < 1000; i++) {
new Thread(() -> processTask()).start();
}
Работать будет. Пока задач немного.

А теперь представим, что таких задач не 1000, а 100 000.
Создавать отдельный Thread на каждую задачу — плохая идея: потоки потребляют память, а большое количество потоков приводит к переключениям контекста и конкуренции за CPU.

Что делает ExecutorService?
Мы отделяем задачу от потока, который её выполнит.

ExecutorService executor =
Executors.newFixedThreadPool(10);

for (int i = 0; i < 1000; i++) {
int taskId = i;

executor.submit(() -> processTask(taskId));
}

Здесь мы создали всего 10 рабочих потоков, а задач отправили 1000.

Остальные задачи будут ждать своей очереди.
Это и есть главное преимущество пула: количество одновременно работающих потоков контролируется.

А что делать с результатом?

Если задача должна что-то вернуть — используем Callable:

Future<String> future = executor.submit(() -> {
return loadUserName(42);
});

String name = future.get();

Future позволяет:
- получить результат;
- дождаться завершения;
- проверить отмену;
- отменить задачу.

Но есть важный момент:
future.get();
блокирует текущий пото
к, пока задача не завершится.

Поэтому бездумно ставить get() везде тоже не стоит.

А если задача упала?
При использовании submit() исключение не падает прямо в вызывающий поток.

Оно сохраняется внутри Future:

Future<?> future = executor.submit(() -> {
throw new RuntimeException("Что-то пошло не так");
});

try {
future.get();
} catch (ExecutionException e) {
System.err.println("Задача завершилась с ошибкой");

}
Это одна из причин, почему ошибки в фоновых задачах иногда «исчезают» из логов.
И самое важное — shutdown
Пул нужно корректно закрывать:

executor.shutdown();

try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
executor.shutdownNow();

}
shutdown() — перестаём принимать новые задачи, но даём уже отправленным завершиться.

shutdownNow() — пытаемся прервать выполняющиеся задачи через interrupt() и возвращаем задачи, которые ещё не начали выполняться.

Но interrupt() — не убийство потока.

Если ваш код игнорирует прерывание, shutdownNow() не гарантирует мгновенную остановку.

Какой размер пула выбрать?

Для CPU-bound задач:
количество потоков ≈ количество CPU-ядер

Для IO-bound задач поток большую часть времени ждёт сеть, БД или диск, поэтому потоков обычно может быть больше. но формула вида cores * 2 — это не закон.

Размер пула зависит от:
- времени CPU;
- времени ожидания;
- нагрузки;
- количества одновременных задач;
- внешних ограничений БД/API.

И вот здесь начинается самое интересное.
Пул потоков — это не просто способ не писать new Thread(
) - Это механизм управления конкуренцией.
Если поставить слишком маленький пул — задачи будут стоять в очереди.
Если слишком большой — можно положить CPU, БД или внешний API.
Поэтому в production важно контролировать не только количество потоков, но и очередь задач, время выполнения и политику отказа.

А Executors.newFixedThreadPool() — хороший способ познакомиться с ExecutorService, но в production часто нужен более явный контроль через ThreadPoolExecutor.
Просмотры: 53
Открыть в MAX Сообщить о неточности

Репосты публикации

Публикация канала

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти