Очередь ThreadPoolExecutor заполнена, достигнут максимум потоков, а для отклонённых задач выбран CallerRunsPolicy. В каком потоке выполнится новая задача?
Новая задача выполнится синхронно в потоке, который вызвал execute() или submit(), если исполнитель ещё не завершает работу. CallerRunsPolicy не создаёт дополнительный поток, а заставляет вызывающий код временно выполнять задачу самостоятельно.
Пулы потоков отделяют отправку задач от их выполнения, но ограниченные очереди и число рабочих потоков требуют реакции на перегрузку. Политика CallerRunsPolicy появилась как простой способ не терять задачи и одновременно замедлять производителя задач.
В отличие от безусловного отбрасывания, этот подход создаёт естественное противодавление: поток, отправляющий задачи, тратит время на их выполнение и временно снижает скорость отправки новых задач.
Если очередь заполнена и все рабочие потоки заняты, пул не может немедленно принять новую задачу. При неправильной политике её можно потерять, отклонить исключением или накопить неограниченное число задач, что увеличивает задержки и расход памяти.
CallerRunsPolicy решает проблему ценой переноса нагрузки на вызывающий поток. Если таким потоком является поток HTTP-запроса, обработка запроса может заметно замедлиться; если это поток, критичный для прогресса системы, можно получить нежелательную цепочку блокировок или рекурсивного выполнения.
При отказе принять задачу CallerRunsPolicy проверяет, что ThreadPoolExecutor не находится в состоянии завершения. Если пул работает, обработчик вызывает run() переданного объекта Runnable непосредственно в текущем потоке. Поэтому новая задача не попадает в рабочую очередь и не мигрирует в другой поток.
Для submit() переданный Callable обычно оборачивается в FutureTask. При срабатывании CallerRunsPolicy в вызывающем потоке выполняется именно эта обёртка; после возврата из submit() её Future уже может быть завершён.
Если пул уже остановлен, стандартный CallerRunsPolicy задачу не выполняет и не выбрасывает исключение. Это важное ограничение: политика подходит для противодействия перегрузке, но сама по себе не гарантирует доставку задачи при завершении исполнителя.
Минимальный пример механизма:
При насыщении пула имя последующей задачи может оказаться именем потока-отправителя, например main. Точный момент насыщения зависит от состояния рабочего потока и очереди, поэтому пример демонстрирует механизм, а не фиксированный порядок вывода.
Главный компромисс — между сохранением задачи и предсказуемостью задержек. CallerRunsPolicy полезна для коротких задач и контролируемых производителей, но опасна для долгих операций, блокирующего ввода-вывода и пользовательских потоков.
Сервис принимает события и отправляет их в ограниченный пул для обогащения. При всплеске нагрузки очередь заполняется. Рассматривались три варианта: AbortPolicy быстро сигнализирует об отказе, но требует отдельной логики повторной доставки; DiscardPolicy снижает нагрузку, но молча теряет события; CallerRunsPolicy сохраняет событие, однако увеличивает задержку потока-производителя.
Для некритичных событий выбрали контролируемое отбрасывание с метрикой потерь, а для обязательных коротких задач — CallerRunsPolicy вместе с ограничением времени обработки и мониторингом задержки. Такое разделение лучше, чем применять одну политику ко всем задачам: цена перегрузки и требования к надёжности у операций различаются.
CallerRunsPolicy выполняет отклонённую задачу?Нет. Если исполнитель уже завершает работу, стандартная политика ничего не делает. Поэтому при остановке пула задача может быть потеряна без исключения; надёжную доставку нужно обеспечивать на уровне протокола или очереди сообщений.
CallerRunsPolicy, что задача будет выполнена только после освобождения рабочего потока?Нет. При отказе постановки в очередь она запускается сразу в вызывающем потоке. Это не ожидание свободного рабочего потока, а синхронное выполнение внутри операции отправки, поэтому вызов execute() или submit() может занять время выполнения самой задачи.
execute() и submit()?При execute() исключение из задачи выполняется в вызывающем потоке и обычно выходит из вызова run(), если задача была запущена через CallerRunsPolicy. При submit() исключение сохраняется внутри Future; его можно получить через get(), а до этого оно не обязано попасть в обработчик необработанных исключений потока. Это различие связано с обёрткой FutureTask, а не с самой политикой отказа.