Cooperative cancellation — interrupt và InterruptedException
Java không kill được thread từ bên ngoài. interrupt() chỉ set một lá cờ; thread đích tự chọn điểm dừng. Xử lý InterruptedException đúng mà không nuốt tín hiệu hủy.
TL;DR: Java không có cách nào kill một thread từ bên ngoài một cách an toàn — Thread.stop() đã deprecated từ 1.2 và ném UnsupportedOperationException từ Java 20. Thay vào đó là cooperative cancellation: bên ngoài chỉ yêu cầu dừng, thread đích tự chọn điểm dừng an toàn. Cơ chế là interrupt status — một lá cờ mỗi thread. interrupt() chỉ set cờ, không dừng gì; thread CPU-bound phải tự check isInterrupted(), còn blocking method (sleep, join, take) khi bị interrupt sẽ ném InterruptedException và xóa cờ. Bắt được exception đó chỉ có hai cách hợp lệ: ném lại (throws), hoặc khôi phục cờ bằng Thread.currentThread().interrupt(). Catch rỗng làm tín hiệu hủy biến mất — thread pool gọi shutdown sẽ chờ mãi.
Bài trước: start() tạo thread mới, join() chờ nó xong, và blocking method như sleep() đưa thread vào TIMED_WAITING. Bài này trả lời câu hỏi thứ tư còn nợ: khi thread đang chạy (hoặc đang ngủ), làm sao bảo nó dừng giữa chừng một cách an toàn?
TicketFlow đang load sơ đồ ghế thì admin bấm shutdown — làm sao dừng thread seats-loader giữa chừng? Đây là câu hỏi mà mọi bài sau của module đều quay về mỗi khi chữ InterruptedException xuất hiện. Câu trả lời của Java không phải "kill nó đi", mà là một mô hình hợp tác tinh tế hơn — và pattern xử lý đúng chỉ hơn pattern sai đúng một dòng code, nhưng đó là khác biệt giữa shutdown trong 1 giây và phải kill -9.
1. Vì sao không thể kill một thread?
Câu trả lời của Java: không có cách nào kill một thread từ bên ngoài một cách an toàn. Method Thread.stop() bị deprecated từ Java 1.2, và từ Java 20 mọi lời gọi ném UnsupportedOperationException. Lý do khai tử: kill một thread tại một điểm tùy ý nghĩa là lock nó đang giữ bị nhả ra trong khi dữ liệu được lock bảo vệ đang sửa dở — invariant vỡ, mọi thread khác nhìn thấy trạng thái hỏng. Một thread bị stop giữa chừng book() có thể để lại số vé đã trừ nhưng booking chưa ghi.
Thay vào đó, Java chọn cooperative cancellation — hủy có hợp tác: bên ngoài chỉ yêu cầu dừng, còn thread đích tự quyết định đâu là điểm dừng an toàn (sau khi xong record hiện tại, sau khi nhả lock, sau khi đóng file). Một bên gửi tín hiệu, bên kia định kỳ kiểm tra và thoát có trật tự.
flowchart LR
A["Thread khac goi<br/>t.interrupt()"] -->|"set co"| B["interrupt status = true"]
B --> C{"Thread dich dang lam gi?"}
C -->|"chay CPU-bound"| D["tu check isInterrupted()<br/>o diem dung an toan"]
C -->|"block trong sleep/join/take"| E["method nem<br/>InterruptedException + xoa co"]
D --> F["don dep -> ket thuc run() co trat tu"]
E --> F
style B fill:#FCD34D
style F fill:#6EE7B72. interrupt() chỉ set một lá cờ
Mỗi thread mang một bit trạng thái gọi là interrupt status — lá cờ "có ai đó đã yêu cầu tôi dừng". Ba method xoay quanh nó:
| Method | Làm gì |
|---|---|
t.interrupt() | Set cờ của thread t thành true. Chỉ vậy — không dừng, không kill, không ném gì vào code đang chạy bình thường |
t.isInterrupted() | Đọc cờ của t, không xóa cờ |
Thread.interrupted() (static) | Đọc cờ của thread hiện tại và xóa nó — dùng khi bạn chuẩn bị xử lý yêu cầu dừng |
Trước khi xem code, tự trả lời:
Một thread chạy vòng lặp tính toán thuần — không sleep, không wait, không blocking call, và không tự check gì. Thread khác gọi interrupt() lên nó. Vòng lặp phản ứng thế nào? Viết ra dự đoán trước khi đọc tiếp.
interrupt() không có quyền lực gì lên thread không thèm kiểm tra cờ. Một vòng lặp tính toán thuần chạy tiếp như chưa hề có cuộc gọi interrupt — cờ được set nhưng không ai đọc:
// Vong lap nay KHONG dung duoc bang interrupt() -- no khong bao gio nhin co
while (true) {
crunchNumbers();
}
// Phien ban hop tac: kiem tra co moi vong lap
while (!Thread.currentThread().isInterrupted()) {
crunchNumbers();
}
// thoat vong lap -> don dep -> ket thuc run() co trat tu
Với công việc CPU-bound, bạn phải tự cài điểm kiểm tra cờ ở chỗ dừng được an toàn. Tần suất là một trade-off: check dày thì phản hồi hủy nhanh, check thưa thì hủy trễ.
3. InterruptedException: khi blocking method nhìn cờ hộ bạn
Còn thread đang ngủ thì sao — đang sleep(), join(), hay take() trên blocking queue (bài 15)? Chúng không chạy vòng lặp nào để check cờ, nên blocking method của JDK làm hộ: thread đang block trong chúng mà bị interrupt thì method ném InterruptedException để nó tỉnh dậy ngay, đồng thời — chi tiết hay bị bỏ qua — xóa luôn interrupt status.
Vì sao là checked exception? Vì "bị yêu cầu hủy giữa chừng" không phải bug mà là tình huống bình thường mọi blocking method đều có thể gặp, và người gọi bắt buộc phải có chính sách đối phó. Checked exception ép bạn viết chính sách đó ra thành code — cách JDK nói: "method này biết chờ, nên nó cũng phải biết bị hủy."
Vì sao exception lại xóa cờ? Vì tín hiệu interrupt đã được giao tận tay bạn dưới dạng exception; cờ đã xong nhiệm vụ đưa tin. Từ đây quyền quyết định nằm ở code của bạn: kết thúc luôn, hoặc khôi phục lại cờ để tầng trên còn nhìn thấy tín hiệu. Nếu cờ không bị xóa, một vòng lặp có sleep sẽ ném InterruptedException mãi mãi ở mọi vòng sau dù bạn đã xử lý xong.
4. Pattern xử lý đúng — và pitfall nuốt interrupt
Quy tắc chỉ có hai nhánh, tùy vị trí của bạn trong call stack.
Nhánh 1 — lan truyền: method không đủ thẩm quyền quyết định "hủy thì làm gì" (code thư viện, code tầng giữa) thì khai báo throws InterruptedException, để exception đi lên:
// Tang giua: khong tu quyet -- day trach nhiem len caller
public SeatMap loadSeatMaps() throws InterruptedException {
return seatMapQueue.take(); // co the block, co the bi interrupt
}
Nhánh 2 — khôi phục cờ: nếu không thể throw (đang trong Runnable.run(), trong vòng lặp worker), xử lý tại chỗ rồi dựng lại lá cờ vừa bị xóa. Thử tự điền dòng còn thiếu trước khi xem đáp án:
Trong khối catch dưới, worker cần làm gì để vòng while ngoài (điều kiện isInterrupted()) biết đã có yêu cầu hủy và thoát? Nhớ rằng blocking method vừa xóa cờ khi ném exception. Viết ra một dòng bạn sẽ đặt vào chỗ /* ... */ trước khi đọc tiếp.
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
SeatMap map = seatMapQueue.take();
process(map);
} catch (InterruptedException e) {
/* ... buoc khoi phuc tin hieu huy ... */
}
}
closeResources(); // diem thoat co trat tu
}
Dòng cần điền là Thread.currentThread().interrupt() — gọi interrupt() lên chính thread hiện tại để dựng lại lá cờ vừa bị blocking method xóa. Nhờ đó vòng lặp while kế tiếp thấy isInterrupted() trả true và thoát có trật tự:
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag -> vong while thay co va thoat
}
Pitfall phổ biến nhất toàn bộ Java concurrency là nuốt interrupt bằng catch rỗng:
// SAI -- nuot tin hieu huy: co da bi xoa, khong restore, khong throw
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// bo trong "cho het warning"
}
Sau catch rỗng, tín hiệu hủy biến mất khỏi vũ trụ: cờ đã bị xóa khi exception ném ra, bạn không khôi phục, không ai phía trên còn cách nào biết từng có yêu cầu dừng — thread pool gọi shutdown sẽ chờ mãi mãi. Bản đúng chỉ hơn đúng một dòng Thread.currentThread().interrupt(), nhưng đó là khác biệt giữa hệ thống shutdown được trong 1 giây và hệ thống phải kill -9.
Gặp InterruptedException chỉ có hai lựa chọn hợp lệ: rethrow (khai báo throws) hoặc khôi phục cờ bằng Thread.currentThread().interrupt(). Catch rỗng, hoặc catch chỉ để log rồi đi tiếp, đều làm tín hiệu hủy biến mất. Quy tắc này sẽ được nhắc lại ở các bài lock, blocking queue và executor.
5. Liên hệ các bài khác
- Bài 02 — Thread API và vòng đời: nền trực tiếp —
sleep()/join()là các blocking method mà bài này dạy cách hủy; trạng tháiTIMED_WAITING/WAITINGlà lúc interrupt phát huy tác dụng. - Bài 15 — Blocking queue:
take()némInterruptedException— consumer loop dùng đúng pattern khôi phục cờ của bài này. - Bài 17b — Vận hành pool production:
shutdownNow()hủy worker bằng interrupt; nuốtInterruptedExceptiontrong task là lý do pool không bao giờ dừng. - Bài 22 — Structured concurrency: nâng interrupt + join thủ công của bài này thành mô hình scope có cấu trúc, tự truyền hủy xuống task con.
6. 📚 Deep Dive
Spec / reference chính thức:
- Javadoc
java.lang.Thread(Java 21) — đọc kỹ mô tả interrupt status trêninterrupt(),isInterrupted(),interrupted(), và ghi chú "always throws UnsupportedOperationException" củastop(). - JLS §17.2.3 — Interruptions — đặc tả tương tác giữa interrupt và wait set.
- Java Concurrency in Practice, chương 7 (Cancellation and Shutdown) — chương kinh điển về cancellation policy; bài này là bản rút gọn tinh thần chương đó.
Ghi chú: Javadoc Thread là nguồn chính xác nhất về hành vi "throw rồi xóa cờ" của từng blocking method — khi nghi ngờ một method có clear interrupt status hay không, tra Javadoc của chính method đó.
7. Tóm tắt
- Java không kill thread:
Thread.stop()deprecated từ 1.2, némUnsupportedOperationExceptiontừ Java 20 — kill tại điểm tùy ý làm vỡ invariant đang sửa dở. - Cooperative cancellation: bên ngoài yêu cầu dừng, thread đích tự chọn điểm dừng an toàn.
interrupt()chỉ set interrupt status;isInterrupted()đọc không xóa;Thread.interrupted()đọc và xóa cờ của thread hiện tại.- Vòng lặp CPU-bound phải tự check
isInterrupted()— interrupt vô tác dụng với thread không nhìn cờ. - Blocking method (
sleep,join,wait,take...) phản ứng với interrupt bằng cách némInterruptedExceptionvà xóa cờ. InterruptedExceptionlà checked exception vì hủy là tình huống bình thường mà caller bắt buộc phải có chính sách.- Xử lý
InterruptedExceptionchỉ có hai nhánh hợp lệ: rethrow, hoặc khôi phục cờ bằngThread.currentThread().interrupt(). Không bao giờ catch rỗng.
8. Tự kiểm tra
Q1Vì sao interrupt() không dừng được một vòng lặp tính toán thuần (không có sleep/wait/blocking call nào)?▸
interrupt() không cưỡng chế gì — nó chỉ set một lá cờ (interrupt status) trên thread đích. Cờ chỉ có tác dụng ở hai nơi: code tự đọc nó bằng isInterrupted(), hoặc blocking method của JDK đọc hộ và ném InterruptedException.
Vòng lặp CPU thuần không rơi vào nơi nào: nó không bao giờ block nên không method nào nhìn cờ hộ, và nếu bạn không tự viết check thì cờ cứ nằm đó vô nghĩa. Đây là hệ quả của cooperative cancellation — Java từ chối kill tại điểm tùy ý, nên trách nhiệm cài "điểm dừng an toàn" thuộc về người viết code.
Q2Khác biệt giữa t.isInterrupted() và Thread.interrupted() (static) là gì? Vì sao cần cái xóa cờ?▸
t.isInterrupted() và Thread.interrupted() (static) là gì? Vì sao cần cái xóa cờ?t.isInterrupted() đọc cờ của thread t và không xóa. Thread.interrupted() là static, đọc cờ của thread hiện tại và xóa nó.
Cần bản xóa cờ khi bạn chuẩn bị xử lý yêu cầu hủy và muốn "nhận" tín hiệu một lần rồi tự quyết: nếu không xóa, một vòng lặp check cờ sẽ thấy true mãi và xử lý lặp đi lặp lại. Ngược lại, dùng isInterrupted() (không xóa) khi chỉ muốn quan sát cờ mà không tiêu thụ tín hiệu, để tầng khác vẫn thấy. Chọn nhầm là nguồn bug tinh vi: xóa cờ khi không định xóa làm mất tín hiệu.
Q3Vì sao InterruptedException là checked exception, và vì sao khi ném nó blocking method lại xóa interrupt status?▸
InterruptedException là checked exception, và vì sao khi ném nó blocking method lại xóa interrupt status?Checked vì "bị yêu cầu hủy giữa chừng" không phải bug mà là tình huống bình thường mọi blocking method có thể gặp; checked exception ép caller viết ra chính sách đối phó thành code, thay vì lờ đi.
Xóa cờ vì tín hiệu interrupt đã được giao tận tay bạn dưới dạng exception — cờ đã xong nhiệm vụ đưa tin. Nếu không xóa, một vòng lặp có sleep sẽ ném InterruptedException mãi ở mọi vòng sau dù bạn đã xử lý xong. Xóa cờ trả quyền quyết định về code của bạn: kết thúc luôn, hoặc khôi phục cờ để tầng trên còn thấy tín hiệu.
Q4Khối catch (InterruptedException e) { } rỗng nguy hiểm thế nào? Hai cách xử lý hợp lệ là gì?▸
catch (InterruptedException e) { } rỗng nguy hiểm thế nào? Hai cách xử lý hợp lệ là gì?Khi blocking method ném InterruptedException, nó đồng thời xóa interrupt status — tín hiệu hủy lúc này chỉ còn trong tay bạn dưới dạng exception. Catch rỗng vứt exception đi: cờ đã xóa, exception đã nuốt, tín hiệu hủy biến mất hoàn toàn. Hậu quả: worker chạy tiếp như thường, thread pool gọi shutdown chờ mãi không thấy worker thoát, phải kill cứng.
Hai cách hợp lệ: (1) rethrow — khai báo throws InterruptedException, dùng khi method không đủ thẩm quyền quyết định chính sách hủy; (2) khôi phục cờ — gọi Thread.currentThread().interrupt() trong catch để dựng lại lá cờ cho vòng lặp ngoài hoặc framework thấy.
Q5Trong worker loop while (!Thread.currentThread().isInterrupted()) { try { take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, giải thích chính xác điều gì xảy ra khi thread bị interrupt lúc đang take().▸
while (!Thread.currentThread().isInterrupted()) { try { take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, giải thích chính xác điều gì xảy ra khi thread bị interrupt lúc đang take().Bước 1: take() đang block, thread bị interrupt → take() ném InterruptedException và xóa interrupt status (cờ về false).
Bước 2: khối catch chạy Thread.currentThread().interrupt() → dựng lại cờ (về true). Không có bước này, cờ sẽ ở false và vòng lặp không biết đã có yêu cầu hủy.
Bước 3: hết khối catch, vòng lặp quay lại kiểm tra điều kiện !isInterrupted() → cờ giờ true nên điều kiện false → thoát vòng lặp → chạy closeResources() và kết thúc run() có trật tự. Toàn bộ là cooperative cancellation đúng bài: nhận tín hiệu, khôi phục để không mất, rồi tự thoát ở điểm an toàn.
Q6Khi nào chọn rethrow (throws) và khi nào chọn khôi phục cờ? Điều gì quyết định?▸
Quyết định bằng vị trí trong call stack và thẩm quyền quyết định chính sách hủy. Nếu method của bạn không tự quyết "hủy thì làm gì" — code thư viện, tầng giữa chỉ chuyển tiếp công việc — thì rethrow: khai báo throws InterruptedException đẩy quyết định lên caller có đủ ngữ cảnh hơn.
Nếu bạn không thể throw — đang trong Runnable.run() (chữ ký cố định, không có throws), hoặc trong vòng lặp worker là nơi cuối cùng xử lý — thì khôi phục cờ: xử lý tại chỗ rồi Thread.currentThread().interrupt() để tín hiệu không mất. Nguyên tắc chung: đẩy tín hiệu lên nơi có thẩm quyền; nếu bạn là nơi cuối, đừng để nó rơi.
Bài tiếp theo: Thread safety — khi nhiều thread cùng chạm vào dữ liệu
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên