Как упростить создание и запуск потоков в Spring и какие аннотации использовать
Что проверяют
Знаете ли вы, что в Spring потоки руками не создают, а отдают задачи управляемому пулу, и понимаете ли, как @Async устроен внутри. Следом обычно спрашивают, почему @Async не сработал при вызове из того же класса и куда деваются исключения.
Как отвечать
Каркас такой:
@EnableAsyncи@Async: первая аннотация на конфигурации включает обработку, вторая на методе бина переносит вызов в пул. Работает через прокси, как@Transactional;- возвращаемые типы:
voidилиCompletableFuture<T>.ListenableFutureобъявлен устаревшим в Spring 6, в новом коде его не используют; - пул по умолчанию: чистый Spring без своего исполнителя берёт
SimpleAsyncTaskExecutor, который заводит новый поток на каждую задачу. Spring Boot сам настраиваетThreadPoolTaskExecutor(бинapplicationTaskExecutor), размер задают свойстваspring.task.execution.pool.*. Сspring.threads.virtual.enabled=trueBoot переходит на виртуальные потоки; - свой пул: бин
ThreadPoolTaskExecutorсcorePoolSize,maxPoolSize,queueCapacityи имя в@Async("reportExecutor"), чтобы тяжёлые задачи не забивали общий пул; - исключения: у
CompletableFutureих ловят вexceptionallyилиhandle, уvoidони уходят вAsyncUncaughtExceptionHandler, который задают черезAsyncConfigurer; - периодические задачи:
@EnableSchedulingи@ScheduledсfixedRate,fixedDelayилиcron. Планировщик в Boot по умолчанию однопоточный, это меняетspring.task.scheduling.pool.size.
Для разовой задачи без аннотаций можно внедрить TaskExecutor и вызвать CompletableFuture.supplyAsync(task, executor).
Где спотыкаются
Вызывают @Async метод из соседнего метода того же класса. Вызов идёт мимо прокси, и метод выполняется синхронно.
Не знают, что у пула Boot очередь по умолчанию без ограничения. Пока она не заполнится, maxPoolSize не достигается никогда, а задачи копятся в памяти.
Ждут, что в асинхронный метод доедут транзакция, SecurityContext и MDC. Они привязаны к потоку и сами не переносятся, для передачи контекста есть TaskDecorator.