Вопрос 13 · Раздел 9

Какие типы Thread Pool существуют в Java?

Java предоставляет несколько готовых типов пулов потоков через фабрику Executors. Каждый тип оптимизирован для определённого сценария.

Версии по языкам: English Russian Ukrainian

Junior уровень

Базовое понимание

Java предоставляет несколько готовых типов пулов потоков через фабрику Executors. Каждый тип оптимизирован для определённого сценария.

1. FixedThreadPool

Фиксированное количество потоков. Все задачи выполняются параллельно, остальные ждут в очереди. Выбирайте, когда знаете максимальную нагрузку заранее (например, обработка 10 файлов одновременно).

ExecutorService pool = Executors.newFixedThreadPool(10);

for (int i = 0; i < 100; i++) {
    pool.submit(() -> {
        // Максимум 10 задач выполняются одновременно
        // Остальные 90 ждут в очереди
    });
}
Параметр Значение
Размер пула Фиксированный (10)
Очередь Безразмерная (LinkedBlockingQueue)
Когда использовать Известная, стабильная нагрузка

2. CachedThreadPool

Создаёт новые потоки по необходимости, переиспользует свободные.

ExecutorService pool = Executors.newCachedThreadPool();

pool.submit(() -> {
    // Если нет свободных потоков — создаётся новый
    // Если поток свободен 60 секунд — уничтожается
});
Параметр Значение
Размер пула 0 до Integer.MAX_VALUE (теоретически ~2 млрд, на практике ограничено памятью ОС — обычно 32K потоков максимум на Linux)
Очередь SynchronousQueue (ёмкость 0)
Когда использовать Много коротких задач

3. SingleThreadExecutor

Ровно один поток. Задачи выполняются строго последовательно.

ExecutorService pool = Executors.newSingleThreadExecutor();

pool.submit(() -> System.out.println("Задача 1"));
pool.submit(() -> System.out.println("Задача 2"));
// Всегда: "Задача 1" → "Задача 2"
Параметр Значение
Размер пула 1
Очередь Безразмерная
Когда использовать Последовательное выполнение

4. ScheduledThreadPool

Планирование задач с задержкой или периодически.

ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);

// Выполнить через 10 секунд
pool.schedule(() -> System.out.println("Delayed"), 10, TimeUnit.SECONDS);

// Выполнять каждые 5 секунд
pool.scheduleAtFixedRate(() -> System.out.println("Periodic"), 0, 5, TimeUnit.SECONDS);
Параметр Значение
Размер пула Фиксированный
Очередь DelayedWorkQueue
Когда использовать Периодические задачи

5. WorkStealingPool (Java 8+)

Примечание: в Java 21+ с виртуальными потоками (Project Loom) для I/O-bound задач предпочтительнее Executors.newVirtualThreadPerTaskExecutor(). Виртуальные потоки стоят ~KB вместо ~1MB и могут создаваться миллионами.

Использует ForkJoinPool с алгоритмом work-stealing.

ExecutorService pool = Executors.newWorkStealingPool();
// По умолчанию: число потоков = число ядер CPU
Параметр Значение
Размер пула Число ядер CPU
Алгоритм Work-stealing
Когда использовать Распараллеливание вычислений

Сравнительная таблица

Тип Размер Очередь Лучше для
FixedThreadPool Фиксированный Безразмерная Стабильная нагрузка
CachedThreadPool Динамический SynchronousQueue Короткие задачи
SingleThreadExecutor 1 Безразмерная Последовательность
ScheduledThreadPool Фиксированный DelayedWorkQueue Расписание
WorkStealingPool CPU cores Work-stealing Вычисления

Middle уровень

FixedThreadPool — детали

// Внутренняя реализация
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
        nThreads,                   // corePoolSize
        nThreads,                   // maximumPoolSize
        0L, TimeUnit.MILLISECONDS,  // keepAliveTime
        new LinkedBlockingQueue<Runnable>() // БЕЗРАЗМЕРНАЯ очередь
    );
}

Опасность: Если задачи приходят быстрее, чем обрабатываются, очередь растёт до бесконечности → OutOfMemoryError.

Решение: Создавайте вручную с ограниченной очередью:

ExecutorService safePool = new ThreadPoolExecutor(
    10, 10, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(1000) // Лимит 1000 задач
);

CachedThreadPool — детали

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
        0,                          // corePoolSize
        Integer.MAX_VALUE,          // maximumPoolSize — нет лимита!
        60L, TimeUnit.SECONDS,      // Потоки умирают через 60s
        new SynchronousQueue<Runnable>() // Ёмкость = 0
    );
}

Опасность: При всплеске нагрузки может создать тысячи потоков → OutOfMemoryError: unable to create new native thread.

SingleThreadExecutor — детали

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(
        new ThreadPoolExecutor(
            1, 1, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>()
        )
    );
}

Отличие от newFixedThreadPool(1): гарантирует, что настройки нельзя изменить (инкапсуляция).

ScheduledThreadPool — детали

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);

// Однократное выполнение через задержку
scheduler.schedule(task, 10, TimeUnit.SECONDS);

// Фиксированная частота (от начала предыдущего запуска)
scheduler.scheduleAtFixedRate(task, 0, 5, TimeUnit.SECONDS);

// Фиксированная задержка (от конца предыдущего запуска)
scheduler.scheduleWithFixedDelay(task, 0, 5, TimeUnit.SECONDS);
// scheduleAtFixedRate: если задача длится 7 секунд, а период 5с — 
// следующая запустится НЕМЕДЛЕННО после завершения (пропуск)
// scheduleWithFixedDelay: подождёт 5 секунд ПОСЛЕ завершения предыдущей

WorkStealingPool — детали

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool(
        Runtime.getRuntime().availableProcessors(),
        ForkJoinPool.defaultForkJoinWorkerThreadFactory,
        null,   // UncaughtExceptionHandler
        true    // asyncMode
    );
}

Алгоритм work-stealing:

  • У каждого потока своя очередь задач
  • Если поток закончил свои задачи — “крадёт” из хвоста очереди другого потока
  • Отлично для неравномерных задач, потому что свободный поток не простаивает, а «крадёт» непроработанные задачи у занятых потоков (work-stealing алгоритм).

Какой пул НЕ использовать

  • НЕ CachedThreadPool для долгих задач → thread explosion (тысячи потоков)
  • НЕ FixedThreadPool с LinkedBlockingQueue для непредсказуемой нагрузки → OOM (очередь растёт бесконечно)
  • НЕ SingleThreadExecutor для CPU-bound параллелизма → один поток, нет параллелизма
  • НЕ WorkStealingPool для I/O-bound → блокировка потока блокирует work-stealing

Senior уровень

Когда Executors не подходят

Senior-разработчики часто избегают методов Executors.* и создают ThreadPoolExecutor напрямую.

Почему:

Проблема Executors.* ThreadPoolExecutor вручную
Лимит очереди Нет (OOM риск) Есть (ArrayBlockingQueue)
RejectedExecutionHandler AbortPolicy Любой (CallerRunsPolicy)
ThreadFactory Default Кастомный (имена, приоритеты)
keepAliveTime Фиксированный Настраиваемый

Выбор пула в зависимости от задачи

Тип задачи Рекомендуемый пул Формула
CPU-bound (расчёты) FixedThreadPool N или N+1 ядер
I/O-bound (БД, API) FixedThreadPool или кастомный N * (1 + W/S)
Короткие задачи CachedThreadPool (с лимитом!) maxPoolSize ограничен
Рекурсия / Стримы ForkJoinPool N ядер
Фоновые задачи ScheduledThreadPool Зависит от частоты

Формула sizing для смешанных нагрузок

Threads = Number of Cores * Target CPU Utilization * (1 + Wait time / Service time)

Пример:

  • 8 ядер CPU
  • Целевая утилизация: 80% (0.8)
  • 90% времени ждём (W/S = 9)
Threads = 8 * 0.8 * (1 + 9) = 8 * 0.8 * 10 = 64 потока

Thread Starvation

I/O-bound пул слишком мал (5 потоков):
  → Все 5 потоков ждут ответа от БД
  → Новые запросы ждут в очереди
  → Система "встала"

Решение: увеличить размер пула для I/O-bound задач

Sizing Formula в коде

public class PoolSizer {
    public static int calculatePoolSize(int cores, double targetUtilization,
                                        double waitTime, double serviceTime) {
        return (int) Math.ceil(
            cores * targetUtilization * (1 + waitTime / serviceTime)
        );
    }

    public static void main(String[] args) {
        int cores = Runtime.getRuntime().availableProcessors();
        int poolSize = calculatePoolSize(cores, 0.8, 90, 10);
        // 8 * 0.8 * (1 + 90/10) = 64
        System.out.println("Recommended pool size: " + poolSize);
    }
}

Диагностика

// Мониторинг пула
ThreadPoolExecutor executor = ...;

// Критические метрики
executor.getActiveCount();         // Сколько потоков работают
executor.getQueue().size();        // Растущая очередь = проблема
executor.getCompletedTaskCount();  // Прогресс
executor.getPoolSize();            // Текущий размер

Best Practices

  1. Избегайте Executors.newCachedThreadPool() в production — используйте кастомный с лимитом
  2. Ограничивайте очередь — ArrayBlockingQueue вместо LinkedBlockingQueue
  3. Формула sizing — CPU-bound: N+1, I/O-bound: N * (1 + W/S)
  4. WorkStealingPool — для рекурсивных задач и parallel streams
  5. ScheduledThreadPool — для периодических задач (cron-like)
  6. Мониторьте queue.size() — растущая очередь = деградация
  7. Кастомный ThreadFactory — именование потоков для отладки

🎯 Шпаргалка для интервью

Обязательно знать:

  • 5 типов пулов: FixedThreadPool (фиксированный размер), CachedThreadPool (динамический), SingleThreadExecutor (1 поток), ScheduledThreadPool (по расписанию), WorkStealingPool (work-stealing алгоритм)
  • FixedThreadPool использует LinkedBlockingQueue без лимита — риск OOM при непредсказуемой нагрузке
  • CachedThreadPool: maximumPoolSize = Integer.MAX_VALUE, SynchronousQueue (ёмкость 0) — риск thread explosion
  • WorkStealingPool = ForkJoinPool с asyncMode=true, каждый поток имеет свою deque-очередь
  • scheduleAtFixedRate — от начала предыдущего запуска (может пропустить если задача длинная), scheduleWithFixedDelay — от конца
  • SingleThreadExecutor гарантирует инкапсуляцию — нельзя изменить настройки, в отличие от newFixedThreadPool(1)
  • В production избегайте Executors.* — создавайте ThreadPoolExecutor вручную с ограниченной очередью и RejectedExecutionHandler
  • Формула sizing: CPU-bound = N+1, I/O-bound = N * (1 + W/S), смешанная = N * util * (1 + W/S)

Частые уточняющие вопросы:

  • Чем FixedThreadPool отличается от SingleThreadExecutor? — FixedThreadPool можно перенастроить, SingleThreadExecutor — инкапсулирован (FinalizableDelegatedExecutorService)
  • Почему CachedThreadPool опасен в production? — При всплеске нагрузки создаёт тысячи потоков → unable to create new native thread
  • Как работает work-stealing? — У каждого потока своя deque; свободный поток «крадёт» задачи из хвоста чужой deque (FIFO), владелец берёт с головы (LIFO)
  • Что выбрать для I/O-bound задач? — FixedThreadPool с формулой N * (1 + W/S) или виртуальные потоки (Java 21+)

Красные флаги (НЕ говорить):

  • “CachedThreadPool — мой выбор для production” — опасен без ограничений, нужен maxPoolSize
  • “FixedThreadPool с LinkedBlockingQueue безопасен” — безлимитная очередь = OOM
  • “WorkStealingPool хорош для I/O” — нет, блокировка потока блокирует work-stealing
  • “scheduleAtFixedRate гарантирует интервал” — нет, если задача длиннее периода — следующий запустится немедленно

Связанные темы:

  • [[12. Что такое пул потоков (Thread Pool)]]
  • [[15. Что делает ExecutorService]]
  • [[16. В чём разница между Executors.newFixedThreadPool() и newCachedThreadPool()]]
  • [[17. Что такое ForkJoinPool]]