Как работает ExecutorService в Java
Что проверяют
Понимаете ли вы устройство пула и знаете ли, чем опасны стандартные фабричные методы.
Как отвечать
ExecutorService это абстракция над пулом потоков: вы отправляете задачи, он сам решает, на каком потоке их выполнить. Смысл в том, чтобы не создавать поток на каждую задачу (это дорого и неограниченно) и ограничивать степень параллелизма.
Отправка задач: execute для Runnable без результата, submit возвращает Future, invokeAll выполняет пачку и ждёт всех.
Внутри всё сводится к ThreadPoolExecutor с параметрами: размер ядра, максимальный размер, время простоя дополнительных потоков, очередь задач, фабрика потоков и политика отклонения.
Алгоритм принятия задачи стоит знать, он неочевиден: пока потоков меньше размера ядра, создаётся новый поток. Когда ядро заполнено, задача идёт в очередь. И только когда очередь заполнена, создаются потоки сверх ядра до максимума. Если и это исчерпано, срабатывает политика отклонения.
Отсюда главное практическое предупреждение: фабричные методы Executors опасны в проде.
newFixedThreadPoolиспользует неограниченную очередь. Потоки сверх ядра не создадутся никогда, а очередь будет расти до исчерпания памяти;newCachedThreadPoolсоздаёт неограниченное число потоков под нагрузкой.
Поэтому в рабочем коде создают ThreadPoolExecutor явно, с ограниченной очередью и осмысленной политикой отклонения. Полезная политика это выполнение задачи в вызывающем потоке: она создаёт естественное обратное давление и притормаживает поставщика.
Ещё две вещи, о которых спрашивают: обязательно именовать потоки через фабрику, иначе в дампе будут безликие номера, и помнить, что исключение в задаче, отправленной через submit, молча оседает в Future. Не вызвали get, значит про ошибку не узнали.
Завершение: shutdown перестаёт принимать новые задачи и доделывает принятые, shutdownNow пытается прервать, awaitTermination ждёт.
С Java 21 для задач с блокирующим вводом-выводом появился пул на виртуальных потоках, где ограничение числа потоков перестаёт быть нужным.
Где спотыкаются
Берут newFixedThreadPool и получают рост очереди до нехватки памяти.
Не проверяют результат Future и теряют исключения.
Не закрывают пул, и приложение не завершается.
Не именуют потоки, из-за чего дамп бесполезен.