Xử lý bất đồng bộ Java
Trước khi xem xét Spring @Async, các khái niệm đồng bộ, bất đồng bộ và đa luồng là điều cần thiết. Giả định rằng bạn đã biết các khái niệm này, hãy cùng tìm hiểu cách xử lý bất đồng bộ Java thuần túy thông qua mã. Nếu bạn đã quen thuộc với luồng Java, bạn có thể bỏ qua chương này.
Nếu bạn tạo một chức năng đơn giản để nhận message và in ra bằng phương thức đồng bộ, bạn có thể viết mã như trên. Nếu chuyển sang phương thức bất đồng bộ đa luồng, bạn có thể viết mã nguồn như sau.
Tuy nhiên, phương pháp này rất nguy hiểm vì không thể quản lý Thread. Ví dụ, nếu có 10.000 cuộc gọi cùng lúc, bạn phải tạo 10.000 Thread trong thời gian rất ngắn. Chi phí tạo Thread không hề nhỏ, điều này ảnh hưởng xấu đến hiệu năng của chương trình, thậm chí có thể dẫn đến lỗi OOM. Do đó, cần triển khai Thread Pool để quản lý Thread, và Java cung cấp lớp ExecutorService.
Số lượng Thread tổng thể được giới hạn ở 10 và chúng ta đã có thể thực hiện chính xác xử lý bất đồng bộ theo cách đa luồng mong muốn. Tuy nhiên, với phương pháp này, bạn phải áp dụng phương thức submit() của ExecutorService cho mỗi phương thức mà bạn muốn xử lý bất đồng bộ, vì vậy bạn phải thực hiện các tác vụ sửa đổi lặp đi lặp lại. Nói cách khác, nếu bạn muốn thay đổi một phương thức ban đầu được viết bằng logic đồng bộ thành bất đồng bộ, bạn phải thay đổi logic của chính phương thức đó.
Spring @Async
Phương pháp đơn giản
Chỉ cần thêm chú thích @EnableAsync vào trên lớp Application và chú thích @Async vào trên phương thức logic đồng bộ mà bạn muốn xử lý bất đồng bộ là xong. Tuy nhiên, phương pháp này có vấn đề là không quản lý luồng. Bởi vì cấu hình mặc định của @Async là sử dụng SimpleAsyncTaskExecutor, nó chỉ có vai trò tạo luồng chứ không phải là pool luồng.
Phương pháp sử dụng pool luồng
Trước tiên, hãy loại bỏ @EnableAsync khỏi lớp Application. Nếu lớp Application được cấu hình @EnableAutoConfiguration hoặc @SpringBootApplication, nó sẽ đọc thông tin bean threadPoolTaskExecutor (sẽ được tạo ở bên dưới) của lớp SpringAsyncConfig (được cấu hình @Configuration) trong thời gian chạy.
Bạn có thể đặt kích thước core và max. Khi đó, bạn có thể dự đoán rằng nó sẽ hoạt động với kích thước core ban đầu và nếu không thể xử lý thêm công việc, số luồng sẽ tăng lên đến kích thước max, nhưng không phải vậy.
Nó tạo ra một LinkedBlockingQueue có kích thước Integer.MAX_VALUE bên trong và nếu các luồng có kích thước core không thể xử lý công việc, chúng sẽ chờ trong Queue. Khi Queue đầy, nó sẽ tạo ra các luồng có kích thước max để xử lý.
Nếu bạn cảm thấy không thoải mái khi đặt kích thước Queue thành Integer.MAX_VALUE, bạn có thể đặt queueCapacity. Nếu đặt như ví dụ trên, ban đầu 3 luồng sẽ xử lý công việc và nếu tốc độ xử lý chậm lại, công việc sẽ được đặt vào Queue có kích thước 100 và nếu có thêm yêu cầu, tối đa 30 luồng sẽ được tạo ra để xử lý công việc.
Sau khi hoàn tất cấu hình pool luồng, bạn chỉ cần gắn tên của bean này vào phương thức có chú thích @Async.
Nếu bạn muốn đặt nhiều loại pool luồng, bạn có thể tạo nhiều phương thức tạo bean như threadPoolTaskExecutor() và khi đặt @Async, bạn có thể đưa bean pool luồng mà bạn muốn vào.
Dạng trả về theo kiểu trả về
Trường hợp không có giá trị trả về
Đây là trường hợp phương thức cần được xử lý bất đồng bộ không cần truyền kết quả xử lý. Trong trường hợp này, bạn có thể đặt kiểu trả về của chú thích @Async thành void.
Trường hợp có giá trị trả về
Bạn có thể sử dụng Future, ListenableFuture, CompletableFuture làm kiểu trả về. Bạn có thể gói dạng trả về của phương thức bất đồng bộ bằng new AsyncResult().
[Future]
future.get() có vai trò chờ đợi đến khi kết quả yêu cầu đến mà không bị chặn. Vì vậy, nó trở thành phương thức chặn bất đồng bộ, hiệu năng không tốt. Thông thường, Future không được sử dụng.
[ListenableFuture]
ListenableFuture cho phép bạn xử lý công việc theo cách không chặn thông qua callback. Tham số đầu tiên của phương thức addCallback() xác định phương thức callback hoàn thành công việc và tham số thứ hai xác định phương thức callback lỗi công việc. Lưu ý rằng vì số luồng cơ bản của pool luồng được đặt thành 3, nên bạn có thể thấy rằng tin nhắn "Task Start" được in ra 3 lần đầu tiên.
[CompletableFuture]
Mặc dù chỉ với ListenableFuture, bạn cũng có thể triển khai logic không chặn, nhưng nếu cần callback trong callback, nó sẽ dẫn đến mã rất phức tạp, được gọi là "callback hell".
<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>
Tất nhiên, trong bài học này, chúng ta sẽ không đề cập chi tiết đến CompletableFuture, vì vậy nếu bạn muốn biết thêm về mã xử lý callback phức tạp, hãy tham khảo liên kết nguồn bên dưới.
Nó có khả năng đọc tốt hơn so với việc định nghĩa callback của ListenableFuture và thực hiện đầy đủ chức năng không chặn. Do đó, khi sử dụng @Async, nếu cần giá trị trả về, nên sử dụng CompletableFuture.
Ưu điểm của @Async
Các nhà phát triển có thể viết phương thức theo cách đồng bộ và nếu muốn bất đồng bộ, họ chỉ cần thêm chú thích @Async vào trên phương thức. Do đó, bạn có thể tạo ra mã có khả năng bảo trì tốt đối với đồng bộ và bất đồng bộ.
Lưu ý về @Async
Để sử dụng chức năng @Async, bạn cần khai báo chú thích @EnableAsync, nhưng nếu không cấu hình thêm, nó sẽ hoạt động ở chế độ proxy. Nói cách khác, tất cả các phương thức bất đồng bộ hoạt động bằng chú thích @Async sẽ tuân theo các hạn chế của Spring AOP. Để biết thêm chi tiết, hãy tham khảobài đăng này.
- Ngay cả khi bạn thêm @Async vào phương thức private, AOP cũng sẽ không hoạt động.
- Khi gọi giữa các phương thức trong cùng một đối tượng, AOP sẽ không hoạt động.
Bình luận0