Gymterview
middle

Какого размера должен быть пул потоков?

Оптимальный размер пула потоков зависит от характера задач и доступных ресурсов. Существуют две основные формулы.

Для CPU-bound задач

Задачи, ограниченные скоростью вычислений (сортировка, шифрование, математические вычисления):

Пример
N_threads = N_cpu + 1

где N_cpu – количество доступных процессоров. Дополнительный +1 поток компенсирует моменты, когда один из потоков приостанавливается (page fault, переключение контекста).

Пример
int poolSize = Runtime.getRuntime().availableProcessors() + 1;
ExecutorService executor = Executors.newFixedThreadPool(poolSize);

Для IO-bound задач

Задачи, которые значительную часть времени ожидают I/O (HTTP-запросы, обращения к БД, чтение файлов):

Пример
N_threads = N_cpu × (1 + WT / ST)

где:

  • N_cpu – количество доступных процессоров
  • WT (Wait Time) – среднее время ожидания (I/O, сеть)
  • ST (Service Time) – среднее время вычислений

Пример: 8 ядер, задача тратит 80 мс на ожидание и 20 мс на вычисления:

Пример
N_threads = 8 × (1 + 80/20) = 8 × 5 = 40 потоков

Практические ограничения

Формулы дают теоретический оптимум, но на практике нужно учитывать:

Фактор Влияние
Доступная память Каждый platform-поток потребляет ~512 КБ-1 МБ стека
Пул соединений БД Нет смысла иметь 100 потоков, если пул БД ограничен 20 соединениями
Внешние API Rate limits и ограничения по числу одновременных соединений
Другие пулы в приложении Суммарное число потоков всех пулов не должно перегружать систему
Контейнерные ограничения В Docker/Kubernetes могут быть ограничения по CPU

Подход Virtual Threads (Java 21+)

С виртуальными потоками вопрос размера пула для IO-bound задач теряет актуальность: Executors.newVirtualThreadPerTaskExecutor() создаёт по одному виртуальному потоку на каждую задачу. Виртуальные потоки крайне легковесны (~несколько КБ) и автоматически освобождают carrier-поток при блокирующих операциях.

Однако для CPU-bound задач виртуальные потоки не дают преимуществ, и формула N_cpu + 1 остаётся актуальной.

Практическая рекомендация

Не полагайтесь только на формулы. Используйте нагрузочное тестирование (JMH, Gatling, wrk) для подбора оптимального размера:

  1. Начните с теоретического значения.
  2. Измерьте throughput и latency под нагрузкой.
  3. Постепенно увеличивайте/уменьшайте размер пула.
  4. Найдите точку, после которой добавление потоков не улучшает (или ухудшает) производительность.

Аналогия из жизни. Вопрос «Сколько кассиров нужно в супермаркете?» Если все покупатели приходят с маленькими корзинами (CPU-bound), достаточно столько кассиров, сколько касс. Если покупатели часто отходят за забытыми товарами (IO-bound), нужно больше кассиров, потому что часть из них простаивает в ожидании.

На собеседовании. Назовите обе формулы: N+1 для CPU-bound и N × (1 + WT/ST) для IO-bound. Обязательно упомяните практические ограничения (память, пул БД). Бонусный ответ: для Java 21+ скажите, что для IO-bound задач можно использовать виртуальные потоки и не беспокоиться о размере пула.