Gymterview
middle

Что будет, если очередь пула потоков уже заполнена, но подаётся новая задача?

Если все потоки пула заняты и очередь задач заполнена и достигнут maximumPoolSize, вступает в действие политика отклонения задач (RejectedExecutionHandler).

Встроенные политики отклонения

Политика Поведение Когда использовать
AbortPolicy (по умолчанию) Выбрасывает RejectedExecutionException Когда потеря задачи недопустима и вызывающий должен об этом узнать
CallerRunsPolicy Задача выполняется в потоке вызывающего (main, HTTP-thread и т.д.) Когда нужен «обратный нажим» (backpressure) – вызывающий поток замедляется
DiscardPolicy Задача молча отбрасывается Когда допустима потеря задач (телеметрия, логи)
DiscardOldestPolicy Удаляется самая старая задача из очереди, новая ставится на её место Когда свежие данные важнее старых

Пример конфигурации

Пример: ThreadPoolExecutor с CallerRunsPolicy
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                          // corePoolSize
    4,                          // maximumPoolSize
    60, TimeUnit.SECONDS,       // keepAliveTime
    new ArrayBlockingQueue<>(10), // Ограниченная очередь на 10 задач
    new ThreadPoolExecutor.CallerRunsPolicy() // Политика отклонения
);

// Если все 4 потока заняты и очередь из 10 задач заполнена:
// 15-я задача выполнится в потоке вызывающего (main)
for (int i = 0; i < 20; i++) {
    final int taskId = i;
    executor.execute(() -> {
        System.out.println("Задача " + taskId
            + " в потоке " + Thread.currentThread().getName());
        try { Thread.sleep(1000); } catch (InterruptedException e) { }
    });
}

executor.shutdown();

Собственная политика отклонения

Можно реализовать RejectedExecutionHandler:

Пример
RejectedExecutionHandler customHandler = (runnable, executor) -> {
    logger.warn("Задача отклонена: очередь переполнена!");
    // Записать в очередь повторных попыток, отправить в Dead Letter Queue и т.д.
};

Важные нюансы

  1. Неограниченная очередь (LinkedBlockingQueue без capacity) – никогда не заполнится, поэтому RejectedExecutionHandler не вызывается, но приложение может упасть с OutOfMemoryError. Пулы newFixedThreadPool() и newSingleThreadExecutor() используют неограниченную очередь – это потенциальная проблема.
  2. submit() vs execute() – при AbortPolicy оба выбрасывают RejectedExecutionException, но submit() сохраняет его в Future.
  3. CallerRunsPolicy как backpressure – самая полезная в production: вызывающий поток замедляется, что естественным образом ограничивает поступление новых задач.

Аналогия из жизни. Ресторан полностью занят и все столики зарезервированы. Пришёл новый гость. AbortPolicy – «Извините, мест нет» (отказ). CallerRunsPolicy – «Официант, обслужи гостя сам, прямо у стойки» (вызывающий сам делает работу). DiscardPolicy – гость молча разворачивается и уходит. DiscardOldestPolicy – выгоняют того, кто дольше всех сидит.

На собеседовании. Назовите все 4 встроенные политики и опишите когда какую использовать. Подчеркните опасность newFixedThreadPool() с неограниченной очередью – это частая причина OutOfMemoryError в production. Рекомендуйте явно создавать ThreadPoolExecutor с ArrayBlockingQueue и CallerRunsPolicy для production-систем.