Как ExecutorService решает, когда создавать новые потоки с учетом corePoolSize и maximumPoolSize
Что проверяют
Знаете ли вы реальный алгоритм ThreadPoolExecutor.execute(), а не интуитивную картинку «сначала потоки до максимума, потом очередь». Следом почти всегда спрашивают, почему у Executors.newFixedThreadPool() параметр maximumPoolSize ничего не решает и чем это грозит.
Как отвечать
Уточните, что ExecutorService только интерфейс, а решение принимает ThreadPoolExecutor. Для каждой новой задачи он проходит три шага:
- потоков меньше
corePoolSize: создаётся новый поток, даже если остальные простаивают; - ядро заполнено: задача предлагается очереди через
workQueue.offer(). Если очередь её приняла, новый поток не создаётся; - очередь отказала: если потоков меньше
maximumPoolSize, создаётся дополнительный поток, иначе срабатываетRejectedExecutionHandler. По умолчанию этоAbortPolicy, который бросаетRejectedExecutionException.
Главный вывод: потоки сверх ядра появляются только при полной очереди, поэтому с неограниченной LinkedBlockingQueue пул не вырастет выше corePoolSize. Так устроен newFixedThreadPool: при медленных задачах очередь растёт без предела, вплоть до OutOfMemoryError. У newCachedThreadPool обратная картина: ядро ноль, максимум Integer.MAX_VALUE и SynchronousQueue, которая ничего не хранит, так что каждая задача без свободного потока порождает новый.
Лишние потоки умирают после простоя дольше keepAliveTime, потоки ядра живут, пока не включить allowCoreThreadTimeOut(true). В проде пул собирают руками: ограниченная ArrayBlockingQueue, осмысленная политика отказа вроде CallerRunsPolicy, именованная ThreadFactory. Для задач, которые в основном ждут ввода-вывода, с Java 21 есть newVirtualThreadPerTaskExecutor(), где пула в этом смысле нет.
Где спотыкаются
Описывают порядок «ядро, потом до максимума, потом очередь». Так ведёт себя пул Tomcat со своей очередью, а стандартный ThreadPoolExecutor делает наоборот.
Считают, что потоки ядра создаются сразу при старте пула. Без prestartAllCoreThreads() они появляются по одному на первые задачи.
Берут newFixedThreadPool в сервисе под нагрузкой и не думают о неограниченной очереди. Отказа не будет, будут растущие задержка и память.