Обработка Java асинхронно
Прежде чем изучать Spring @Async, необходимо понять концепции синхронности, асинхронности и многопоточности. Предполагается, что вы знакомы с этими понятиями, и мы рассмотрим способ обработки асинхронных задач в чистом Java с помощью кода. Если вы хорошо знакомы с потоками Java, вы можете пропустить эту главу.
Если мы создадим функцию, которая просто принимает сообщение и выводит его на консоль, используя синхронный подход, то код будет выглядеть так, как показано выше. Если изменить этот код, чтобы он работал с асинхронной многопоточной обработкой, то исходный код можно написать следующим образом.
Однако этот метод очень опасен, так как не позволяет управлять потоками. Например, если одновременно будет выполнено 10 000 вызовов, то за очень короткое время потребуется создать 10 000 потоков. Стоимость создания потоков немаленькая, поэтому это может негативно сказаться на производительности программы, и даже привести к ошибке OOM. Поэтому для управления потоками необходимо реализовать пул потоков, и в Java для этого предоставляется класс ExecutorService.
Мы ограничили общее количество потоков 10, и теперь можем правильно использовать асинхронную обработку с помощью многопоточности. Однако в этом методе для каждого метода, который мы хотим обработать асинхронно, необходимо использовать метод submit() класса ExecutorService, что влечет за собой повторяющиеся изменения. То есть, если мы хотим изменить метод, изначально написанный синхронно, на асинхронный, то неизбежно придется изменять логику самого метода.
Spring @Async
Простой способ
Просто добавьте аннотацию @EnableAsync над классом Application и аннотацию @Async над методом синхронной логики, которую вы хотите обработать асинхронно. Однако этот метод не решает проблему управления потоками. Это связано с тем, что по умолчанию @Async использует SimpleAsyncTaskExecutor, который не является пулом потоков, а просто создает потоки.
Использование пула потоков
Сначала удалите @EnableAsync из класса Application. Если в классе Application есть конфигурация @EnableAutoConfiguration или @SpringBootApplication, то во время выполнения будет считана информация о бине threadPoolTaskExecutor из класса SpringAsyncConfig (который мы создадим ниже), так как в нём указана аннотация @Configuration.
Мы можем установить значения core и max. В этом случае мы ожидаем, что сначала будет работать core потоков, а когда они не смогут обработать задачи, количество потоков увеличится до max. Но на самом деле это не так.
Внутренне создается очередь LinkedBlockingQueue размером Integer.MAX_VALUE, и если core потоков не могут обработать задачу, то она помещается в очередь. Когда очередь заполняется, тогда создается max потоков для обработки задач.
Если вам неудобно устанавливать размер очереди равным Integer.MAX_VALUE, вы можете задать значение queueCapacity. В данном случае, сначала будут работать 3 потока, а когда скорость обработки задач снизится, задачи будут помещаться в очередь размером 100, а при поступлении новых задач будет создано максимум 30 потоков для обработки.
Настройка пула потоков завершена. Теперь нам нужно указать имя этого бина в методе, который помечен аннотацией @Async.
Если вы хотите настроить несколько типов пулов потоков, создайте несколько методов, которые создают бины, подобные threadPoolTaskExecutor(), и при использовании @Async указывайте нужный бин пула потоков.
Формат возвращаемого значения в зависимости от типа
Отсутствие возвращаемого значения
В случае, когда методу, который необходимо обработать асинхронно, не нужно передавать результат, в качестве типа возвращаемого значения для аннотации @Async устанавливается void.
Наличие возвращаемого значения
В качестве типа возвращаемого значения можно использовать Future, ListenableFuture, CompletableFuture. Формат возвращаемого значения асинхронного метода нужно заключить в new AsyncResult().
[Future]
future.get() блокирует выполнение и ждет, пока не будет получен результат запроса. Из-за этого получается блокирующий асинхронный подход, который снижает производительность. Обычно Future не используется.
[ListenableFuture]
ListenableFuture позволяет обрабатывать задачи в неблокирующем режиме с помощью обратных вызовов. В методе addCallback() первый параметр — это метод обратного вызова при успешном завершении задачи, а второй — метод обратного вызова при ошибке. Обратите внимание, что мы установили core потоков в пуле потоков равным 3, поэтому можно заметить, что сообщение «Task Start» сначала выводится 3 раза.
[CompletableFuture]
Хотя ListenableFuture позволяет реализовать неблокирующую логику, если требуется обратный вызов внутри обратного вызова, это может привести к очень сложному коду, который называют «ад обратных вызовов».
<span class="image-inline ck-widget" contenteditable="false"><img src="https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9f152db8-c015-43cd-bf13-85c594f5f218%2FUntitled.png?table=block&id=268ac0bc-ca7b-4bcb-b11b-ac611a5038b2&spaceId=b453bd85-cb15-44b5-bf2e-580aeda8074e&width=2000&userId=80352c12-65a4-4562-9a36-2179ed0dfffb&cache=v2" alt="Untitled" style="aspect-ratio:2000/1455;" width="2000" height="1455"></span>
Конечно, в этом уроке мы не будем подробно рассматривать CompletableFuture, поэтому, если вам интересен код для решения проблемы сложных обратных вызовов, обратитесь к ссылке в источнике ниже.
Читабельность кода с определением обратного вызова для ListenableFuture стала лучше, и неблокирующая функциональность реализована полностью. Поэтому, если при использовании @Async требуется возвращаемое значение, рекомендуется использовать CompletableFuture.
Преимущества @Async
Разработчик может написать метод в синхронном стиле, а если понадобится асинхронный подход, то просто добавить аннотацию @Async над этим методом. Таким образом, можно создавать код с хорошей поддержкой синхронного и асинхронного подходов.
Меры предосторожности при использовании @Async
Для использования функции @Async необходимо объявить аннотацию @EnableAsync. Если не настраивать её дополнительно, то она будет работать в режиме прокси. То есть, все асинхронные методы, использующие аннотацию @Async, будут следовать ограничениям Spring AOP. Более подробно об этом можно прочитать в этом посте.
- Если аннотацию @Async применить к приватному методу, AOP работать не будет.
- Если в одном объекте вызвать метод из другого метода, AOP работать не будет.
Комментарии0