![translation](https://cdn.durumis.com/common/trans.png)
To jest post przetłumaczony przez AI.
Wybierz język
Tekst podsumowany przez sztuczną inteligencję durumis
- Opisuje sposób implementacji asynchronicznego przetwarzania Java przy użyciu Spring @Async, jego zalety i kwestie, na które należy zwrócić uwagę.
- Używając Spring @Async, możesz w prosty sposób zaimplementować asynchroniczne przetwarzanie, dodając adnotację @EnableAsync i dodając adnotację @Async do metod, które mają być przetwarzane asynchronicznie.
- Konfiguracja puli wątków umożliwia wydajne zarządzanie wątkami, a typ zwracany pozwala na użycie Future, ListenableFuture, CompletableFuture itp. do zwracania wyników asynchronicznego przetwarzania.
Przetwarzanie asynchroniczne w Javie
Zanim przyjrzymy się Spring @Async, konieczne jest poznanie koncepcji synchronizacji, asynchroniczności i wielowątkowości. Zakładając, że znasz te koncepcje, przyjrzyjmy się kodowi implementującemu asynchroniczne przetwarzanie w czystej Javie. Jeśli jesteś zaznajomiony z wątkami w Javie, możesz pominąć ten rozdział.
public class MessageService {
public void print(String message) {
System.out.println(message);
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Jeśli utworzymy funkcję, która po prostu odbiera wiadomość i ją wypisuje, możemy napisać kod w sposób synchroniczny, jak pokazano powyżej. Zamieniając to na asynchroniczny sposób przetwarzania z użyciem wielowątkowości, możemy napisać kod źródłowy w następujący sposób.
public class MessageService {
public void print(String message) {
new Thread(() -> System.out.println(message))
.start();
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Jednak ta metoda jest bardzo niebezpieczna, ponieważ nie pozwala na zarządzanie wątkami. Na przykład, jeśli jednocześnie zostanie wywołanych 10 000 wywołań, trzeba będzie bardzo szybko utworzyć 10 000 wątków. Tworzenie wątków jest kosztowne, co może negatywnie wpłynąć na wydajność programu, a nawet spowodować błąd OOM. Dlatego do zarządzania wątkami należy zaimplementować pulę wątków. Java oferuje klasę ExecutorService.
public class MessageService {
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
public void print(String message) {
executorService.submit(() -> System.out.println(message));
}
}
public class Main {
public static void main(String[] args) {
MessageService messageService = new MessageService();
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Ograniczyliśmy całkowitą liczbę wątków do 10, a także możemy prawidłowo przeprowadzić asynchroniczne przetwarzanie wielowątkowe, które chcemy. Jednak powyższa metoda wymaga zastosowania metody submit() klasy ExecutorService do każdej metody, którą chcemy przetworzyć asynchronicznie, co wymaga powtarzalnych modyfikacji. Oznacza to, że jeśli chcemy zamienić metodę pierwotnie zapisaną w sposób synchroniczny na asynchroniczny, musimy zmienić samą logikę metody.
Spring @Async
Prosty sposób
@EnableAsync
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@Service
public class MessageService {
@Async
public void print(String message) {
System.out.println(message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() {
for (int i = 1; i <= 100; i++) {
messageService.print(i + "");
}
}
Wystarczy dołączyć adnotację @EnableAsync nad klasą aplikacji, a następnie adnotację @Async nad metodą, której logikę chcemy przetworzyć asynchronicznie. Jednak powyższa metoda nie zarządza wątkami. Ponieważ domyślnym ustawieniem @Async jest użycie SimpleAsyncTaskExecutor, który nie jest pulą wątków, a jedynie tworzy wątki.
Metoda korzystająca z puli wątków
Najpierw usuń @EnableAsync z klasy aplikacji. Jeśli klasa aplikacji ma ustawienie @EnableAutoConfiguration lub @SpringBootApplication, w czasie wykonywania odczyta informacje o beanach threadPoolTaskExecutor z klasy SpringAsyncConfig (która zostanie utworzona poniżej), która ma ustawione @Configuration.
@Configuration
@EnableAsync // Powinna być dołączona do klasy konfiguracji asynchronicznej, a nie do aplikacji.
public class SpringAsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3); // Liczba wątków podstawowych
taskExecutor.setMaxPoolSize(30); // Maksymalna liczba wątków
taskExecutor.setQueueCapacity(100); // Rozmiar kolejki
taskExecutor.setThreadNamePrefix("Executor-");
return taskExecutor;
}
Możesz ustawić rozmiary rdzenia i maks. Możesz oczekiwać, że będzie działać z liczbą rdzeni, a następnie zwiększać liczbę wątków do rozmiaru maks., jeśli nie będzie możliwe przetworzenie dalszych zadań, ale tak nie jest.
Wewnętrznie tworzy kolejkę LinkedBlockingQueue o rozmiarze Integer.MAX_VALUE, więc jeśli wątki rdzenia nie będą w stanie przetworzyć zadań, zaczną czekać w kolejce. Gdy kolejka zostanie zapełniona, utworzone zostaną wątki o rozmiarze maks., aby przetworzyć zadania.
Jeśli ustawienie rozmiaru kolejki na Integer.MAX_VALUE jest zbyt obciążające, możesz ustawić queueCapacity. W powyższym przypadku, zaczynając od 3 wątków, będzie przetwarzać, a jeśli prędkość przetwarzania będzie spadać, będzie przechowywać zadania w kolejce o rozmiarze 100, a jeśli będzie więcej żądań, utworzy maksymalnie 30 wątków, aby przetworzyć zadania.
Po skonfigurowaniu puli wątków należy dodać nazwę tego beana do metody z adnotacją @Async.
@Service
public class MessageService {
@Async("threadPoolTaskExecutor")
public void print(String message) {
System.out.println(message);
}
Jeśli chcesz ustawić wiele typów puli wątków, utwórz wiele metod tworzenia beanów, takich jak threadPoolTaskExecutor(), a następnie dodaj żądany bean puli wątków podczas ustawiania @Async.
Typ zwracany przez różne typy zwrotne
Gdy nie ma wartości zwrotnej
Jest to przypadek, gdy metoda, którą należy przetworzyć asynchronicznie, nie musi przekazywać wyniku. W tym przypadku należy ustawić typ zwracany adnotacji @Async na void.
Gdy jest wartość zwrotna
Jako typ zwracany można użyć typów Future, ListenableFuture i CompletableFuture. W przypadku asynchronicznej metody można zamknąć formę zwrotną w new AsyncResult().
[Future]
@Service
public class MessageService {
@Async
public Future print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws ExecutionException, InterruptedException {
for (int i = 1; i <= 5; i++) {
Future future = messageService.print(i + "");
System.out.println(future.get());
}
}
}
// Wynik wykonania
Task Start - 1
jayon-1
Task Start - 2
jayon-2
Task Start - 3
jayon-3
Task Start - 4
jayon-4
Task Start - 5
future.get() blokuje i czeka, aż wynik żądania zostanie zwrócony. W rezultacie staje się to asynchronicznym sposobem blokowania, co wpływa na wydajność. Zazwyczaj Future nie jest używane.
[ListenableFuture]
@Service
public class MessageService {
@Async
public ListenableFuture print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message);
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws InterruptedException {
for (int i = 1; i <= 5; i++) {
ListenableFuture listenableFuture = messageService.print(i + "");
listenableFuture.addCallback(System.out::println, error -> System.out.println(error.getMessage()));
}
}
}
// Wynik wykonania
Task Start - 1
Task Start - 3
Task Start - 2
jayon-1
jayon-2
Task Start - 5
jayon-3
Task Start - 4
jayon-4
ListenableFuture umożliwia przetwarzanie zadań w sposób nieblokujący za pomocą wywołania zwrotnego. Pierwszy parametr metody addCallback() to metoda wywołania zwrotnego po zakończeniu zadania, a drugi to metoda wywołania zwrotnego po błędzie. Należy zauważyć, że ponieważ rozmiar rdzenia puli wątków jest ustawiony na 3, można zauważyć, że komunikat „Task Start” jest wyświetlany początkowo 3 razy.
[CompletableFuture]
ListenableFuture umożliwia implementację logiki nieblokującej, ale jeśli potrzebne są wywołania zwrotne w wywołaniach zwrotnych, może to prowadzić do bardzo złożonego kodu, nazywanego „piekłem wywołań zwrotnych”.
Oczywiście w tym przypadku nie będziemy szczegółowo omawiać CompletableFuture, więc jeśli jesteś ciekawy kodu, który radzi sobie ze złożonymi wywołaniami zwrotnymi, zajrzyj do linku w źródle poniżej.
@Service
public class MessageService {
@Async
public CompletableFuture print(String message) throws InterruptedException {
System.out.println("Task Start - " + message);
Thread.sleep(3000);
return new AsyncResult<>("jayon-" + message).completable();
}
}
@RequiredArgsConstructor
@RestController
public class MessageController {
private final MessageService messageService;
@GetMapping("/messages")
@ResponseStatus(code = HttpStatus.OK)
public void printMessage() throws InterruptedException {
for (int i = 1; i <= 5; i++) {
CompletableFuture completableFuture = messageService.print(i + "");
completableFuture
.thenAccept(System.out::println)
.exceptionally(error -> {
System.out.println(error.getMessage());
return null;
});
}
}
Jest bardziej czytelny niż definiowanie wywołań zwrotnych w ListenableFuture i w pełni obsługuje funkcje nieblokujące. Dlatego zaleca się użycie CompletableFuture podczas korzystania z @Async, jeśli potrzebna jest wartość zwrotna.
Zalety @Async
Deweloperzy mogą pisać metody w sposób synchroniczny, a następnie dodawać adnotację @Async nad metodą, jeśli chcą użyć sposobu asynchronicznego. Dzięki temu można tworzyć kod, który jest łatwy w utrzymaniu i działa synchronicznie i asynchronicznie.
Punkty do zapamiętania dotyczące @Async
Aby korzystać z funkcji @Async, należy zadeklarować adnotację @EnableAsync, ale jeśli nie zostanie dokonana żadna dodatkowa konfiguracja, będzie działać w trybie proxy. Oznacza to, że wszystkie metody asynchroniczne, które działają z adnotacją @Async, będą podlegać ograniczeniom Spring AOP. Aby uzyskać więcej informacji, zapoznaj się ztym wpisem.
- Nawet jeśli adnotację @Async zostanie dodana do metody prywatnej, AOP nie zadziała.
- AOP nie zadziała podczas wywoływania metody przez tę samą instancję.