1. Khởi chạy tác vụ bất đồng bộ: supplyAsync và runAsync
Cách phổ biến nhất để khởi chạy một tác vụ bất đồng bộ — sử dụng CompletableFuture.supplyAsync. Phương thức này nhận một lambda hoặc phương thức trả về kết quả. Ví dụ, ta muốn mô phỏng việc tải dữ liệu từ máy chủ:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Mô phỏng thao tác lâu (ví dụ: tải tệp)
sleep(1000);
return "Dữ liệu từ máy chủ";
});
System.out.println("Tác vụ đã được khởi chạy!");
// ... có thể làm việc khác trong khi tác vụ đang chạy
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
runAsync: khi không cần kết quả
Nếu tác vụ của bạn không trả về gì (ví dụ chỉ ghi log, gửi thông báo), hãy sử dụng runAsync:
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
sleep(500);
System.out.println("Thao tác đã hoàn tất!");
});
runAsync luôn trả về CompletableFuture<Void>, vì không có kết quả nào được mong đợi.
2. thenApply, thenAccept, thenRun: khác nhau như thế nào?
Khi một tác vụ bất đồng bộ hoàn tất, thường bạn muốn làm gì đó với kết quả. Để làm điều này, có các phương thức “trình xử lý”:
- thenApply — chuyển đổi kết quả và trả về một kết quả mới.
- thenAccept — nhận kết quả, không trả về gì (dùng cho tác dụng phụ).
- thenRun — không nhận kết quả, không trả về gì (chỉ thực thi một hành động sau khi tác vụ hoàn tất).
thenApply: xử lý và chuyển đổi kết quả
Nếu bạn cần chuyển đổi kết quả của tác vụ trước đó, hãy sử dụng thenApply. Ví dụ, đã tải một chuỗi và bây giờ muốn biết độ dài của nó:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Java");
CompletableFuture<Integer> lengthFuture = future.thenApply(s -> {
System.out.println("Đang tính độ dài chuỗi...");
return s.length();
});
// lengthFuture giờ chứa Integer — độ dài của chuỗi "Java"
lengthFuture.thenAccept(len -> System.out.println("Độ dài: " + len));
Điều gì xảy ra:
- future chứa chuỗi "Java".
- thenApply chuyển chuỗi thành độ dài của nó (int).
- thenAccept in ra kết quả.
thenAccept: hành động với kết quả (không trả về gì)
Nếu chỉ cần làm gì đó với kết quả (ví dụ in ra màn hình) và không cần trả về gì — hãy dùng thenAccept:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Xin chào, thế giới!");
future.thenAccept(result -> {
System.out.println("Kết quả: " + result);
});
thenAccept giống như một “consumer”: nó tiêu thụ kết quả và làm điều hữu ích với nó.
thenRun: hành động không cần kết quả
Nếu bạn muốn chỉ thực hiện một hành động sau khi tác vụ hoàn tất, nhưng không cần kết quả, hãy dùng thenRun:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hoàn tất!");
future.thenRun(() -> {
System.out.println("Tải xong!");
});
Lưu ý: bên trong thenRun bạn không thể lấy kết quả của tác vụ trước đó — nó bị bỏ qua.
3. Chuỗi lời gọi: xây dựng pipeline từ các tác vụ
Sức mạnh lớn nhất của CompletableFuture — là khả năng xây dựng chuỗi tính toán. Mỗi phương thức (thenApply, thenAccept, thenRun) trả về một CompletableFuture mới, và bạn có thể tiếp tục gắn trình xử lý vào đó.
Ví dụ: xử lý nhiều bước
Hãy hoàn thiện ứng dụng của chúng ta: tải dữ liệu, chuyển đổi, in kết quả và ghi log rằng mọi thứ đã hoàn tất.
CompletableFuture.supplyAsync(() -> {
System.out.println("Bước 1: Đang tải dữ liệu...");
sleep(500);
return "Java";
})
.thenApply(data -> {
System.out.println("Bước 2: Đang chuyển đổi dữ liệu...");
return data.toUpperCase();
})
.thenAccept(result -> {
System.out.println("Bước 3: In kết quả: " + result);
})
.thenRun(() -> {
System.out.println("Bước 4: Mọi thứ đã hoàn tất!");
});
Kết quả trên console:
Bước 1: Đang tải dữ liệu...
Bước 2: Đang chuyển đổi dữ liệu...
Bước 3: In kết quả: JAVA
Bước 4: Mọi thứ đã hoàn tất!
Lưu ý:
Mỗi bước tiếp theo chỉ bắt đầu sau khi bước trước hoàn thành. Điều này cho phép xây dựng các “dây chuyền” xử lý dữ liệu thực thụ.
4. Biến thể bất đồng bộ: thenApplyAsync, thenAcceptAsync, thenRunAsync
Mặc định, các trình xử lý (thenApply, thenAccept, thenRun) được thực thi trong cùng luồng nơi tác vụ trước đó kết thúc. Đôi khi điều này không tiện — nếu xử lý nặng, tốt hơn nên chuyển nó sang một luồng khác.
Vì vậy có các phiên bản bất đồng bộ:
- thenApplyAsync
- thenAcceptAsync
- thenRunAsync
Khác nhau ở đâu?
- Không có Async: trình xử lý có thể chạy trong cùng luồng với tác vụ trước đó (ví dụ nếu tác vụ kết thúc trong ForkJoinPool, trình xử lý cũng ở đó).
- Có Async: trình xử lý chắc chắn sẽ được thực thi trong một luồng khác từ ForkJoinPool (hoặc Executor của bạn).
Ví dụ: so sánh xử lý thường và xử lý bất đồng bộ
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("Đang tải... [" + Thread.currentThread().getName() + "]");
return "Hello";
});
future.thenApply(result -> {
System.out.println("thenApply: [" + Thread.currentThread().getName() + "]");
return result + " World";
});
future.thenApplyAsync(result -> {
System.out.println("thenApplyAsync: [" + Thread.currentThread().getName() + "]");
return result + " Async World";
});
Kết quả điển hình:
Đang tải... [ForkJoinPool.commonPool-worker-1]
thenApply: [ForkJoinPool.commonPool-worker-1]
thenApplyAsync: [ForkJoinPool.commonPool-worker-2]
Kết luận:
Trình xử lý bất đồng bộ chạy ở một luồng khác.
Khi nào dùng các phương thức Async?
- Nếu xử lý tốn tài nguyên (ví dụ tính toán phức tạp, làm việc với mạng).
- Nếu không muốn chặn luồng nơi tác vụ trước đó vừa kết thúc.
- Nếu muốn quản lý luồng một cách tường minh (ví dụ truyền Executor của riêng bạn như tham số thứ hai).
5. Những điểm hữu ích
Bảng: so sánh các phương thức thenApply, thenAccept, thenRun
| Phương thức | Dùng kết quả? | Trả về giá trị? | Dùng để làm gì |
|---|---|---|---|
|
Có | Có | Chuyển đổi kết quả |
|
Có | Không | Tác dụng phụ (in ra, ghi log) |
|
Không | Không | Chỉ hành động sau khi tác vụ hoàn tất |
|
Có | Có | Tương tự, nhưng ở luồng khác |
|
Có | Không | Tương tự, nhưng ở luồng khác |
|
Không | Không | Tương tự, nhưng ở luồng khác |
Câu hỏi: xây dựng chuỗi dài như thế nào?
Có thể gọi các phương thức nối tiếp nhau, như lắp LEGO:
CompletableFuture.supplyAsync(() -> "42")
.thenApply(Integer::parseInt)
.thenApply(x -> x * 2)
.thenAccept(x -> System.out.println("Kết quả: " + x));
Kết quả:
Kết quả: 84
Mỗi bước tiếp theo nhận kết quả của bước trước đó, có thể thay đổi nó hoặc chỉ đơn giản là sử dụng.
6. Các lỗi thường gặp khi làm việc với thenApply, thenAccept, thenRun
Lỗi số 1: Nhầm lẫn về kiểu giá trị trả về.
thenApply phải trả về một giá trị để chuyển tiếp trong chuỗi. Nếu bạn vô tình dùng thenApply nhưng không trả về kết quả, thao tác tiếp theo sẽ nhận null (hoặc thậm chí không biên dịch được). Với tác dụng phụ, hãy dùng thenAccept hoặc thenRun.
Lỗi số 2: Cố gắng dùng kết quả trong thenRun.
Bên trong thenRun không có quyền truy cập vào kết quả của tác vụ trước đó. Nếu muốn dùng kết quả, hãy chọn thenApply hoặc thenAccept.
Lỗi số 3: Chặn luồng chính.
Nếu bạn gọi get() hoặc join() trong luồng chính, bạn sẽ mất mọi lợi thế của bất đồng bộ: luồng sẽ chờ tác vụ hoàn tất, như trong mã đồng bộ kiểu cũ. Tốt hơn là dùng các chuỗi không chặn và callback.
Lỗi số 4: Không xử lý lỗi.
Nếu trong chuỗi xảy ra ngoại lệ mà bạn không thêm trình xử lý (exceptionally, handle, whenComplete), nó sẽ “bị mất”, và tác vụ có thể kết thúc lỗi mà bạn không thấy. Luôn xử lý lỗi trong các chuỗi.
Lỗi số 5: Thực thi ngoài ý muốn ở luồng khác.
Các phương thức bất đồng bộ (thenApplyAsync và tương tự) có thể chạy trong luồng khác. Nếu bạn truy cập vào biến không được bảo vệ cho truy cập đa luồng, có thể xảy ra race condition.
GO TO FULL VERSION