1. Future: “Đợi khi nào sẵn sàng”
Các thao tác bất đồng bộ trong Java giống như gửi thư qua bưu điện: bạn không đứng chờ ở hòm thư để đợi phản hồi, mà đi làm việc khác. Khi thư đến — bạn chỉ việc nhận thông báo. Trong mã, bạn cần hiểu khi nào thao tác kết thúc và lấy kết quả hoặc lỗi. Trong Java có hai cách tiếp cận: qua Future và qua CompletionHandler.
Với Future bạn nhận một “phiếu hẹn” — lời hứa sẽ có kết quả trong tương lai. Bạn có thể thỉnh thoảng kiểm tra xem đã xong chưa, hoặc chờ lấy kết quả bằng cách gọi get().
Với CompletionHandler bạn không cần chờ: chỉ định sẵn một bộ xử lý, và nó sẽ tự được gọi — khi thành công hoặc khi có lỗi.
Hoạt động như thế nào?
Khi bạn gọi phương thức bất đồng bộ read() hoặc write() của AsynchronousFileChannel, bạn có thể nhận một đối tượng kiểu Future<Integer>. Nó giống như số thứ tự trong hàng đợi điện tử: thao tác đã bắt đầu, và bạn có thể hỏi bất kỳ lúc nào: “Xong chưa?”.
Chữ ký phương thức
Future<Integer> read(ByteBuffer dst, long position)
hoặc
Future<Integer> write(ByteBuffer src, long position)
- dst — bộ đệm để đọc dữ liệu vào.
- src — bộ đệm để ghi dữ liệu ra.
- position — vị trí trong tệp để đọc/ghi.
Ví dụ: Đọc bất đồng bộ với Future
Đoạn mã đọc bất đồng bộ 1024 byte đầu tiên từ tệp và chờ kết quả một cách tường minh:
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
import java.io.IOException;
public class FutureReadExample {
public static void main(String[] args) {
Path path = Path.of("example.txt");
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Bắt đầu đọc bất đồng bộ
Future<Integer> future = channel.read(buffer, 0);
// ... có thể làm việc khác trong khi tệp đang được đọc
// Chờ thao tác hoàn tất (lệnh gọi chặn!)
int bytesRead = future.get(); // Có thể ném InterruptedException, ExecutionException
System.out.println("Số byte đã đọc: " + bytesRead);
// Chuyển sang đọc từ bộ đệm
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
} catch (Exception e) {
System.err.println("Lỗi đọc tệp: " + e);
}
}
}
- Phương thức future.get() chặn luồng cho đến khi thao tác hoàn tất — giống như “ngồi chờ điện thoại” cho đến khi có cuộc gọi.
- Có thể gọi future.isDone() để biết thao tác đã xong chưa, rồi mới gọi get().
- Nếu thao tác kết thúc với lỗi, get() sẽ ném ExecutionException (bên trong là nguyên nhân).
Khi nào nên dùng Future?
Future phù hợp khi bạn muốn khởi chạy một tác vụ và sau đó, khi thuận tiện, chờ lấy kết quả: đọc tệp, gửi yêu cầu, tính toán nền. Cách tiếp cận này cũng tiện để chạy nhiều thao tác song song rồi gom kết quả sau.
Nhưng nếu cần tính bất đồng bộ thực sự, không chặn và có thông báo tự động khi hoàn tất, hãy cân nhắc CompletionHandler.
2. CompletionHandler: “Gọi tôi khi xong”
CompletionHandler là một interface mà bạn hiện thực và truyền vào phương thức read() hoặc write(). Khi thao tác kết thúc (hoặc xảy ra lỗi), Java sẽ tự gọi bộ xử lý của bạn. Giống như để lại số điện thoại: “Gọi tôi khi xong”.
Chữ ký phương thức
void read(ByteBuffer dst,
long position,
A attachment,
CompletionHandler<Integer, ? super A> handler)
- dst — bộ đệm để đọc.
- position — vị trí trong tệp.
- attachment — đối tượng tùy ý sẽ được truyền vào handler (có thể là null hoặc, ví dụ, tên tệp).
- handler — bộ xử lý của bạn.
Giao diện CompletionHandler
public interface CompletionHandler<V, A> {
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
}
- completed được gọi khi thành công; result — số byte, attachment — đối tượng của bạn.
- failed được gọi khi có lỗi; exc — ngoại lệ.
Ví dụ: Đọc bất đồng bộ với CompletionHandler
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
public class CompletionHandlerReadExample {
public static void main(String[] args) throws IOException {
Path path = Path.of("example.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, "example.txt", new CompletionHandler<Integer, String>() {
@Override
public void completed(Integer result, String attachment) {
System.out.println("Tệp " + attachment + " đã được đọc, số byte: " + result);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
closeChannel();
}
@Override
public void failed(Throwable exc, String attachment) {
System.err.println("Lỗi đọc tệp " + attachment + ": " + exc);
closeChannel();
}
private void closeChannel() {
try {
channel.close();
} catch (IOException e) {
System.err.println("Lỗi đóng kênh: " + e);
}
}
});
// Lưu ý: phương thức main có thể kết thúc trước khi việc đọc hoàn tất nếu không "giữ" luồng!
// Trong ứng dụng thực, luồng thường không kết thúc ngay (ví dụ: server, UI).
try {
Thread.sleep(500); // Dành thời gian cho thao tác bất đồng bộ (chỉ cho ví dụ!)
} catch (InterruptedException ignored) {}
}
}
Chúng ta truyền vào read() một handler ẩn danh biết cần làm gì sau khi hoàn tất. Trong completed nhận kết quả và xử lý nó, còn trong failed — phản ứng khi có lỗi. Đừng quên đóng kênh bên trong handler để tránh rò rỉ tài nguyên.
Có một điểm cần lưu ý: phương thức main có thể kết thúc sớm hơn thao tác bất đồng bộ, nên ví dụ có thêm Thread.sleep(500) — chỉ để nhìn thấy kết quả. Trong ứng dụng thực thường không cần mẹo này.
Khi nào nên dùng CompletionHandler?
CompletionHandler phù hợp khi cần bất đồng bộ thực sự, không chờ đợi và không chặn: bạn khởi chạy thao tác và mô tả cần làm gì khi kết thúc. Điều này rất quan trọng với UI (JavaFX, Swing) để giao diện không “đơ”, và hữu ích trên server — các luồng không ngồi chờ mà chỉ dùng khi có việc.
4. So sánh Future và CompletionHandler
| Cách tiếp cận | Chặn luồng? | Khi nào dùng? | Ví dụ kịch bản |
|---|---|---|---|
| Future | Có (khi get()) | Xử lý tuần tự đơn giản | Sao chép tệp, báo cáo |
| CompletionHandler | Không | Thực sự bất đồng bộ, UI, server, thao tác song song | Server, GUI, IO hàng loạt |
- Future — dễ hiểu hơn, nhưng cần chặn để nhận kết quả.
- CompletionHandler — phức tạp hơn một chút, nhưng mang lại tính bất đồng bộ thực sự và không chặn các luồng.
5. Thực hành: Ghi bất đồng bộ vào tệp với CompletionHandler
Ví dụ ghi chuỗi vào tệp bất đồng bộ và thông báo kết quả:
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class CompletionHandlerWriteExample {
public static void main(String[] args) throws IOException {
String text = "Xin chào, thế giới bất đồng bộ!";
ByteBuffer buffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
Path path = Path.of("async_output.txt");
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
channel.write(buffer, 0, path, new CompletionHandler<Integer, Path>() {
@Override
public void completed(Integer result, Path attachment) {
System.out.println("Đã ghi thành công " + result + " byte vào tệp " + attachment);
try {
channel.close();
} catch (IOException e) {
System.err.println("Lỗi đóng kênh: " + e);
}
}
@Override
public void failed(Throwable exc, Path attachment) {
System.err.println("Lỗi ghi vào tệp " + attachment + ": " + exc);
try {
channel.close();
} catch (IOException e) {
System.err.println("Lỗi đóng kênh: " + e);
}
}
});
// Giữ main lại để thấy kết quả (chỉ cho ví dụ)
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {}
}
}
GO TO FULL VERSION