Какие паттерны проектирования используются для кастомизации поведения при переполнении очереди задач
Что проверяют
Знаете ли вы, что в java.util.concurrent поведение при переполнении уже вынесено в отдельную точку расширения, и можете ли назвать паттерн, который за ней стоит. Следом почти всегда спрашивают, когда задача вообще отклоняется и почему Executors.newFixedThreadPool до этого не доживает.
Как отвечать
Начните с механики ThreadPoolExecutor: новая задача получает свой поток, пока их меньше corePoolSize, затем ложится в очередь, и только когда очередь полна, пул растёт до maximumPoolSize. Если заняты и очередь, и все потоки (или пул уже остановлен), вызывается RejectedExecutionHandler.
Дальше паттерны:
- Стратегия: сам
RejectedExecutionHandler. Реализацию передают в конструктор, пул о ней ничего не знает; - готовые стратегии:
AbortPolicy(по умолчанию, бросаетRejectedExecutionException),CallerRunsPolicy(задачу выполняет поток, который её прислал, это естественное обратное давление),DiscardPolicy(молча выбрасывает),DiscardOldestPolicy(выкидывает самую старую задачу из очереди и пробует снова); - Декоратор: обёртка над любой политикой, которая пишет метрику и лог, прежде чем передать решение дальше;
- Цепочка обязанностей: попробовать резервный пул, затем сохранить задачу в базу или брокер и только потом отказать;
- Producer-Consumer с ограниченной очередью:
put()уArrayBlockingQueueблокирует производителя, аoffer(e, timeout, unit)даёт ему уйти по таймауту.
У Executors.newVirtualThreadPerTaskExecutor() очереди нет, каждая задача сразу получает виртуальный поток, поэтому нагрузку там ограничивают Semaphore на входе.
Где спотыкаются
Берут Executors.newFixedThreadPool и рассуждают о политиках отказа. Внутри там неограниченная LinkedBlockingQueue: отказа не будет никогда, а при всплеске нагрузки приложение упадёт с OutOfMemoryError.
Выбирают DiscardPolicy без метрики. Задачи теряются, и никто об этом не узнаёт, пока не пожалуются пользователи.
Ставят CallerRunsPolicy, не думая, чей это поток. Если задачи шлёт поток обработки HTTP-запросов или event loop, тяжёлая задача заблокирует именно его, и задержка вырастет у всех клиентов.