Зачем пул
Создавать new Thread на каждую задачу опасно: потоки дороги, и при всплеске нагрузки их число (а с ним и память/переключения контекста) выходит из-под контроля. Пул переиспользует ограниченное число потоков и ставит избыточные задачи в очередь.
ExecutorService отделяет ЧТО делать (задача Runnable/Callable) от ТОГО, как и где это выполняется (политика пула). Это и абстракция, и предохранитель.
Параметры ThreadPoolExecutor
ThreadPoolExecutor конфигурируется: corePoolSize (постоянные потоки), maximumPoolSize (потолок), keepAliveTime (через сколько лишние сверх core простаивающие потоки гибнут), workQueue (очередь задач), ThreadFactory (имена потоков, daemon) и RejectedExecutionHandler (что делать при переполнении).
Логика приёма задачи: сначала заполняются core-потоки; когда они заняты — задачи копятся в очереди; только если очередь заполнена, создаются потоки до maximum; если и это исчерпано — срабатывает политика отказа (AbortPolicy по умолчанию бросает исключение, CallerRunsPolicy исполняет в вызывающем потоке, давая backpressure).
// Так пул конфигурируют в проде: границы явные, отказ осознанный
ExecutorService pool = new ThreadPoolExecutor(
8, 16, // core / maximum потоков
60, TimeUnit.SECONDS, // сколько живёт лишний поток без работы
new ArrayBlockingQueue<>(1000), // ОГРАНИЧЕННАЯ очередь — иначе OOM вместо отказа
new ThreadFactoryBuilder().setNameFormat("orders-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // перегрузка тормозит отправителя
);
// Порядок роста: сначала до core, потом в очередь, и только при полной очереди — до maximum

Ловушки фабрик Executors
Удобные фабрики опасны в проде. newFixedThreadPool и newSingleThreadExecutor используют НЕограниченную LinkedBlockingQueue — при наплыве задачи копятся до OutOfMemoryError. newCachedThreadPool создаёт НЕограниченное число потоков — при всплеске исчерпает ресурсы.
Поэтому в серьёзном коде создают ThreadPoolExecutor явно — с ограниченной очередью и осознанной политикой отказа, чтобы система деградировала предсказуемо, а не падала.
Выбор размера пула
Размер зависит от природы задач. CPU-bound: примерно число доступных ядер (Runtime.availableProcessors()) ± 1 — больше потоков лишь добавят переключений. IO-bound: потоков может быть значительно больше, потому что они в основном ждут; ориентир — число ядер × (1 + время_ожидания / время_счёта).
Это отправная точка, которую подтверждают нагрузочным тестом, а не финальная формула.
Жизненный цикл и планирование
Пул нужно закрывать: shutdown() перестаёт принимать задачи и доисполняет очередь, shutdownNow() пытается прервать выполняемые и возвращает невыполненные; затем awaitTermination ждёт завершения. Незакрытый пул с не-daemon потоками не даст JVM завершиться.
submit() возвращает Future, invokeAll() запускает пачку задач. ScheduledExecutorService планирует отложенные и периодические задачи (scheduleAtFixedRate/scheduleWithFixedDelay) — современная замена java.util.Timer, который не переживал исключения и работал в одном потоке.
Как отвечать на собеседовании
Объясните, как ThreadPoolExecutor принимает задачу (core → очередь → max → политика отказа), назовите ловушку фабрик (неограниченные очередь/потоки → OOM) и почему предпочитают явный пул с ограниченной очередью. Дайте ориентиры размера для CPU- и IO-bound и не забудьте про корректный shutdown/awaitTermination.