1. Thread.interrupt() và hủy hợp tác
Trong ứng dụng thực tế, tác vụ có thể chạy lâu, thậm chí “treo” — khi làm việc với mạng, tệp hoặc dịch vụ bên ngoài. Người dùng có thể hủy thao tác, máy chủ có thể dừng xử lý yêu cầu, hoặc đơn giản là timeout chung hết hạn. Nếu không biết hủy tác vụ đúng cách, ứng dụng sẽ đơ, tiêu tốn tài nguyên vô ích và phản ứng kém với sự kiện bên ngoài.
Ý tưởng chính: hủy phải mang tính hợp tác — chính tác vụ cần tự kiểm tra xem có được yêu cầu kết thúc hay không và giải phóng tài nguyên một cách an toàn.
Thread.interrupt() hoạt động như thế nào
Mỗi thread có một cờ “bị ngắt”. Khi bạn gọi thread.interrupt(), cờ này được đặt thành true. Bản thân thread không bị “kill” ngay; nó phải tự kiểm tra trạng thái và kết thúc: định kỳ gọi Thread.currentThread().isInterrupted() và thoát một cách chuẩn mực.
Ví dụ:
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Làm việc...
try {
Thread.sleep(100); // Có thể bị ngắt
} catch (InterruptedException e) {
// Cờ bị xóa, nhưng ta có thể tự ngắt lại
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Luồng kết thúc do bị ngắt.");
});
worker.start();
// ... sau đó
worker.interrupt();
Cờ hoạt động tự động ở đâu?
- Các phương thức có thể bị chặn (sleep, wait, join, các thao tác trên cấu trúc blocking) sẽ ném InterruptedException khi bị ngắt.
- Trong các trường hợp khác (ví dụ, vòng lặp tính toán), cần tự kiểm tra isInterrupted().
Mẫu “đặt cờ — và thoát nhanh”
- Ở mã gọi: thread.interrupt()
- Trong tác vụ: kiểm tra định kỳ Thread.currentThread().isInterrupted()
- Nếu cần — giải phóng tài nguyên và kết thúc một cách chuẩn mực.
Lỗi thường gặp: kỳ vọng interrupt() sẽ “giết” thread ngay lập tức. Không — đó chỉ là tín hiệu, tác vụ phải tự phản hồi.
2. Future.cancel(), CancellationException và hủy tác vụ
Future.cancel hoạt động thế nào
Khi bạn chạy tác vụ qua ExecutorService.submit(), bạn nhận được một đối tượng Future. Nó có phương thức cancel(boolean mayInterruptIfRunning):
- Nếu tác vụ chưa bắt đầu — nó sẽ không được chạy.
- Nếu tác vụ đang chạy và mayInterruptIfRunning == true — sẽ gọi interrupt() trên thread đang thực thi tác vụ.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Công việc dài
}
System.out.println("Tác vụ kết thúc do bị hủy.");
});
// ... sau đó
future.cancel(true); // Yêu cầu hủy tác vụ
Điều gì thực sự xảy ra với tác vụ
Hủy qua Future không phải là nút “giết thread” kỳ diệu, về bản chất nó là hình thức lịch sự của Thread.interrupt(). Nếu tác vụ kiểm tra cờ ngắt đúng cách — nó sẽ kết thúc gọn gàng. Nếu không — nó sẽ tiếp tục chạy cho đến khi kết thúc tự nhiên.
Nếu gọi future.get() sau khi hủy, bạn sẽ nhận CancellationException — nhắc rằng tác vụ đã bị gỡ bỏ.
3. CompletableFuture: hủy, timeout và chuỗi
Hủy CompletableFuture
CompletableFuture cũng có cancel(boolean). Nếu tác vụ chưa hoàn thành, nó sẽ bị hủy và tất cả các handler tiếp theo (thenApply, thenAccept, v.v.) sẽ không được gọi.
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Làm việc...
}
System.out.println("CF kết thúc do bị hủy.");
});
// ... sau đó
cf.cancel(true);
Timeout: orTimeout và completeOnTimeout
- orTimeout(timeout, unit) — kết thúc CompletableFuture với TimeoutException nếu không xong kịp thời gian.
- completeOnTimeout(value, timeout, unit) — hoàn thành với giá trị chỉ định nếu không xong kịp thời gian.
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(5000); } catch (InterruptedException e) {}
return "OK";
});
cf.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> "TIMEOUT")
.thenAccept(System.out::println); // Sau 2 giây: "TIMEOUT"
Lan truyền hủy trong chuỗi
Nếu hủy CompletableFuture “ở trên cùng”, các bước tiếp theo trong chuỗi sẽ không được gọi. Nhưng khi dùng thenCompose để khởi chạy các thao tác bất đồng bộ bên trong, việc hủy không tự động “bật ngược lên trên” — bạn cần thiết kế rõ ràng (kiểm tra trạng thái, hủy các tác vụ con, dùng deadline chung).
Cẩn thận với thenCompose và Executor tùy biến! Hãy đảm bảo các tác vụ bên trong biết phản hồi ngắt/hủy và/hoặc nhận được timeout chung.
4. StructuredTaskScope: hủy nhóm tác vụ
Structured Concurrency và hủy
StructuredTaskScope (Java 21+) cho phép chạy một nhóm tác vụ và quản lý vòng đời của chúng như một khối thống nhất. Nếu một tác vụ lỗi hoặc hết timeout — các tác vụ còn lại sẽ tự động bị hủy.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
scope.join(); // chờ tất cả tác vụ hoàn thành
scope.throwIfFailed(); // nếu có ít nhất một cái lỗi — ném exception
String result = f1.resultNow() + f2.resultNow();
System.out.println(result);
}
- Nếu bất kỳ tác vụ nào lỗi — scope hủy tất cả các tác vụ còn lại.
- Nếu hết timeout (qua scope.joinUntil(deadline)) — scope hủy mọi tác vụ.
Chính sách kết thúc
- ShutdownOnFailure — hủy tất cả tác vụ khi gặp lỗi đầu tiên.
- ShutdownOnSuccess — hủy các tác vụ còn lại ngay khi có một tác vụ thành công.
5. Thực hành: hủy an toàn các thao tác dài
Ví dụ: hủy IO blocking
Nếu tác vụ bị chặn khi đọc từ tệp hoặc mạng, ngắt thread không phải lúc nào cũng hiệu quả — một số thao tác IO không phản hồi với interrupt. Trong các API hiện đại (NIO, AsynchronousFileChannel) việc ngắt được hỗ trợ tốt hơn, nhưng vẫn chưa đồng nhất.
Khuyến nghị:
- Dùng IO không chặn nếu cần khả năng hủy.
- Với IO blocking — đặt timeout ở mức API (ví dụ, Socket.setSoTimeout).
- Với tác vụ bất đồng bộ — dùng Future.cancel và phản hồi ngắt một cách đúng đắn.
Ví dụ: hủy khi chờ queue/barrier
Nhiều bộ đồng bộ (BlockingQueue.take(), CountDownLatch.await(), CyclicBarrier.await()) sẽ ném InterruptedException khi bị ngắt. Trong handler, hãy bắt exception, khôi phục cờ nếu cần và kết thúc tác vụ một cách chuẩn mực.
6. Mẫu “time‑budget”: deadline chung cho một nhóm thao tác
Trong các ứng dụng phức tạp, thường cần đặt một timeout chung cho cả nhóm thao tác. Ví dụ, nếu người dùng chỉ chờ tối đa 2 giây, còn bên trong cần thực hiện 3 yêu cầu mạng — tất cả phải hoàn thành trong cùng một deadline.
Truyền deadline xuống dưới ngăn xếp như thế nào?
- Truyền đối tượng deadline (ví dụ, Instant deadline) vào mọi phương thức có thể blocking.
- Ở mỗi phương thức, tính thời gian còn lại: Duration.between(Instant.now(), deadline).
- Dùng khoảng thời gian này cho timeout trong các thao tác blocking (await(timeout), poll(timeout), orTimeout(timeout)).
Instant deadline = Instant.now().plusSeconds(2);
void doWork(Instant deadline) throws TimeoutException, InterruptedException {
Duration left = Duration.between(Instant.now(), deadline);
if (left.isNegative() || left.isZero()) throw new TimeoutException();
// Sử dụng left cho timeout
queue.poll(left.toMillis(), TimeUnit.MILLISECONDS);
}
Scoped Values / ngữ cảnh
Trong Java 21+, có thể dùng Scoped Values để truyền deadline xuyên suốt ngăn xếp lời gọi, tránh phải truyền tường minh vào từng phương thức.
7. Structured Concurrency: hủy toàn bộ scope khi lỗi/timeout
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
boolean completed = scope.joinUntil(Instant.now().plusSeconds(2));
if (!completed) {
scope.shutdown();
throw new TimeoutException("Deadline đã hết!");
}
scope.throwIfFailed();
// ...
}
- Nếu deadline hết hạn — scope hủy tất cả tác vụ.
- Nếu một tác vụ bị lỗi — các tác vụ còn lại bị hủy tự động.
8. Các lỗi thường gặp khi làm việc với hủy và timeout
Lỗi số 1: Kỳ vọng interrupt() sẽ kết thúc thread ngay lập tức. Thực tế đó chỉ là tín hiệu — tác vụ phải tự kiểm tra trạng thái và kết thúc đúng cách.
Lỗi số 2: Không kiểm tra isInterrupted() trong các vòng lặp dài. Nếu không kiểm tra cờ ngắt, tác vụ sẽ chạy mãi, kể cả khi đã được yêu cầu kết thúc.
Lỗi số 3: Future.cancel() không dẫn đến hủy nếu tác vụ không phản hồi interrupt. Nếu tác vụ “điếc”, cancel() sẽ không giúp.
Lỗi số 4: Không truyền timeout xuống dưới ngăn xếp. Nếu không truyền deadline vào mọi phương thức, thao tác bên trong có thể “treo” lâu hơn mức cần thiết.
Lỗi số 5: Trong chuỗi thenCompose của CompletableFuture việc hủy không tự lan truyền. Nếu hủy future “ở trên”, tác vụ bên trong có thể tiếp tục chạy — hãy xử lý việc hủy một cách tường minh.
Lỗi số 6: StructuredTaskScope không được đóng (không dùng try‑with‑resources). Nếu không đóng scope, các tác vụ con có thể bị “treo” lại.
GO TO FULL VERSION