Thread API và vòng đời — start, join, interrupt
Tạo và chạy thread đúng cách: start() vs run(), 6 trạng thái của Thread.State, join() và sleep(), daemon thread, và quan trọng nhất — interrupt với cooperative cancellation: vì sao interrupt() chỉ set cờ, vì sao InterruptedException là checked, và pattern xử lý đúng.
TL;DR: start() mới tạo thread thật — gọi thẳng run() chỉ là method call thường trên thread hiện tại. Một thread đi qua 6 trạng thái: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. join() chờ thread khác kết thúc; sleep() ngủ có hạn nhưng không nhả lock. Daemon thread bị JVM bỏ rơi khi mọi thread thường đã xong. Quan trọng nhất: Java không có cách kill thread an toàn — interrupt() chỉ set một lá cờ, thread đích tự quyết định điểm dừng. Đó là cooperative cancellation, và pattern xử lý InterruptedException đúng (rethrow hoặc khôi phục cờ, không bao giờ nuốt) là kỹ năng mọi bài sau của module đều dựa vào.
1. Giới thiệu: ba lần đọc file lúc khởi động
Bài trước dừng ở mức khái niệm: process, thread, và lý do thread tồn tại. Bài này cầm tay vào API — vì trước khi nói về thread safety hay lock, bạn cần biết một thread được tạo ra, chạy, chờ, và dừng như thế nào.
Bắt đầu từ một tình huống quen thuộc. Service đặt vé TicketFlow lúc khởi động phải nạp ba nguồn dữ liệu: danh sách sự kiện từ file, bảng giá vé, và sơ đồ ghế. Mỗi thao tác mất khoảng 2 giây vì đọc đĩa và parse. Chạy tuần tự:
public class TicketFlowBootstrap {
public static void main(String[] args) throws Exception {
loadEvents(); // ~2s doc file + parse
loadPricing(); // ~2s
loadSeatMaps(); // ~2s
System.out.println("Ready"); // sau ~6s
}
}
Ba thao tác hoàn toàn độc lập — không cái nào cần kết quả của cái nào — nhưng vẫn xếp hàng ăn trọn 6 giây, trong khi đĩa và CPU phần lớn thời gian ngồi chờ nhau. Nếu mỗi thao tác chạy trên một thread riêng, cả ba tiến triển song song và tổng thời gian xấp xỉ thao tác chậm nhất: ~2 giây. Đó là lời hứa của thread. Nhưng để giữ lời hứa đó mà không tự bắn vào chân, bạn cần trả lời được bốn câu hỏi: tạo thread thế nào, chờ nó xong thế nào, nó đang ở trạng thái gì, và — khó nhất — bảo nó dừng giữa chừng thế nào.
2. Tạo và chạy thread: start() khác run() ở đâu
Cách phổ biến nhất để tạo thread là đưa một Runnable — một mẩu công việc không nhận tham số, không trả kết quả — vào constructor của Thread:
Thread eventsLoader = new Thread(() -> loadEvents(), "events-loader");
eventsLoader.start(); // tu day, loadEvents() chay tren thread moi
Tham số thứ hai là tên thread — luôn nên đặt, vì khi đọc thread dump hay log lỗi, "events-loader" nói lên nhiều điều hơn "Thread-0".
Điểm dễ nhầm nhất với người mới nằm ở chỗ Thread có cả hai method start() và run(), và gọi nhầm cái sau thì chương trình vẫn chạy, không báo lỗi gì — chỉ là chạy sai hoàn toàn so với ý định.
Cơ chế bên dưới của hai method này khác nhau một trời một vực:
start()gọi xuống method nativestart0(), yêu cầu hệ điều hành tạo một OS thread mới (với platform thread, mô hình one-to-one như bài trước). Khi OS thread đó được scheduler cấp CPU, JVM mới invokerun()trên thread mới đó.start()trả về gần như ngay lập tức — nó không chờ công việc chạy xong, thậm chí không chờ công việc bắt đầu.run()gọi trực tiếp thì chỉ là một method call bình thường: code trongRunnablethực thi tuần tự trên chính thread đang gọi, như mọi method khác. Không có thread nào được tạo ra cả.
Thread t = new Thread(() -> {
System.out.println("Run on: " + Thread.currentThread().getName());
}, "worker");
t.run(); // in "Run on: main" -- chi la method call thuong
t.start(); // in "Run on: worker" -- thread moi thuc su chay
Bug "gọi run() thay vì start()" nguy hiểm vì nó âm thầm: ba thao tác load của TicketFlow vẫn hoàn thành đủ, kết quả đúng, chỉ có điều chúng chạy tuần tự trên main thread — vẫn mất 6 giây, và bạn không hiểu vì sao "đã dùng thread" mà không nhanh lên.
Một pitfall API nữa: mỗi Thread object chỉ start() được đúng một lần. Thread đã chạy xong không thể "hồi sinh" — gọi start() lần hai ném IllegalThreadStateException:
Thread t = new Thread(() -> System.out.println("once"));
t.start();
t.start(); // IllegalThreadStateException
Lý do nằm ở thiết kế vòng đời: một Thread object đại diện cho một lần thực thi, đi một chiều từ lúc tạo đến lúc kết thúc, không quay vòng. Muốn chạy lại công việc, tạo Thread mới — hoặc tốt hơn, dùng thread pool tái sử dụng thread (bài 13 — Executor và thread pool).
3. Vòng đời: 6 trạng thái của một thread
Vòng đời một chiều đó được Java mô hình hóa tường minh bằng enum Thread.State — gọi t.getState() bất kỳ lúc nào để xem thread đang ở đâu. Có đúng 6 trạng thái:
stateDiagram-v2
[*] --> NEW : new Thread(...)
NEW --> RUNNABLE : start()
RUNNABLE --> BLOCKED : cho monitor lock
BLOCKED --> RUNNABLE : lay duoc lock
RUNNABLE --> WAITING : join() / wait() / park()
WAITING --> RUNNABLE : thread dich xong / notify / unpark
RUNNABLE --> TIMED_WAITING : sleep(ms) / join(ms) / wait(ms)
TIMED_WAITING --> RUNNABLE : het timeout / co ket qua / interrupt
RUNNABLE --> TERMINATED : run() ket thuc
TERMINATED --> [*]| Trạng thái | Vào khi nào | Ra khi nào | Method liên quan |
|---|---|---|---|
NEW | Vừa new Thread(...), chưa start | Gọi start() | constructor |
RUNNABLE | Đã start — đang chạy hoặc chờ được cấp CPU | Kết thúc, hoặc rơi vào một trạng thái chờ | start() |
BLOCKED | Chờ lấy monitor lock đang bị thread khác giữ | Lấy được lock | synchronized (bài 06) |
WAITING | Chờ vô hạn một sự kiện từ thread khác | Sự kiện xảy ra: thread đích xong, được notify, được unpark | join(), wait(), LockSupport.park() |
TIMED_WAITING | Chờ có thời hạn | Hết timeout, sự kiện xảy ra, hoặc bị interrupt | sleep(ms), join(ms), wait(ms) |
TERMINATED | run() kết thúc — trả về bình thường hoặc ném exception | Không bao giờ ra — trạng thái cuối | — |
Ba điều đáng dừng lại:
RUNNABLE không có nghĩa là "đang chạy". Nó gộp cả hai tình huống: thread đang thực thi trên một core, và thread sẵn sàng chạy nhưng đang xếp hàng chờ scheduler cấp CPU. JVM không phân biệt hai trường hợp này vì việc cấp CPU là chuyện của OS, thay đổi từng mili-giây.
BLOCKED và WAITING khác nhau về bản chất. BLOCKED chỉ dành riêng cho việc chờ monitor lock — thread muốn vào vùng synchronized mà lock đang trong tay thread khác. WAITING là chờ một sự kiện: thread khác kết thúc (join), một thông báo (notify), một tín hiệu unpark. Khi debug một thread dump, phân biệt này nói cho bạn biết hệ thống đang kẹt vì tranh lock hay vì chờ việc — hai bệnh khác nhau, hai cách chữa khác nhau.
TERMINATED là một chiều. Khớp với pitfall start-hai-lần ở trên: không có cạnh nào từ TERMINATED quay về NEW hay RUNNABLE trong sơ đồ.
4. join() và sleep() — hai kiểu chờ
4.1 join(): chờ thread khác xong
Quay lại TicketFlow. Ba loader đã chạy song song, nhưng main thread phải biết khi nào cả ba xong mới được in "Ready" và mở cổng nhận request. Công cụ cho việc đó là join() — thread gọi t.join() sẽ dừng lại (vào WAITING) cho đến khi thread t kết thúc:
public static void main(String[] args) throws InterruptedException {
Thread events = new Thread(TicketFlowBootstrap::loadEvents, "events-loader");
Thread pricing = new Thread(TicketFlowBootstrap::loadPricing, "pricing-loader");
Thread seats = new Thread(TicketFlowBootstrap::loadSeatMaps, "seats-loader");
events.start();
pricing.start();
seats.start(); // ca ba chay song song tu day
events.join(); // main cho tung loader xong
pricing.join();
seats.join();
System.out.println("Ready"); // sau ~2s thay vi ~6s
}
Thứ tự ba lệnh join() không quan trọng — main chỉ cần cả ba xong, và tổng thời gian chờ vẫn bằng loader chậm nhất.
Cơ chế bên dưới của join() thú vị ở chỗ nó không phải magic: bản cài đặt truyền thống trong JDK là một vòng lặp wait() trên monitor của chính Thread object đó — thread gọi join ngủ trên monitor của t, và khi t kết thúc, JVM gọi notifyAll() trên t để đánh thức mọi thread đang join. (Cơ chế wait/notify sẽ được mổ kỹ ở bài 06 — volatile & synchronized; giờ chỉ cần biết join được xây trên nó.) Chi tiết này có một hệ quả thực dụng: Javadoc khuyên đừng bao giờ wait()/notify() trên Thread object — bạn sẽ giẫm chân lên cơ chế nội bộ của join.
join() còn có bản giới hạn thời gian join(millis) — chờ tối đa chừng đó rồi bỏ cuộc (vào TIMED_WAITING thay vì WAITING). Luôn cân nhắc bản này cho code production: chờ vô hạn một thread có thể không bao giờ xong là một cách treo hệ thống.
4.2 sleep(): ngủ có hạn, và không nhả gì cả
Thread.sleep(millis) đưa thread hiện tại vào TIMED_WAITING trong ít nhất millis mili-giây — "ít nhất" vì hết giờ thread chỉ trở lại RUNNABLE, còn bao giờ được cấp CPU lại là việc của scheduler.
Điều quan trọng nhất về sleep() lại là điều nó không làm: nó không nhả bất kỳ lock nào thread đang giữ. Thread ngủ trong vùng synchronized thì ôm lock ngủ luôn — mọi thread khác cần lock đó đứng BLOCKED chờ đủ giấc của nó. Đây là khác biệt then chốt với wait() (nhả lock trước khi ngủ — chi tiết ở bài 06), và là lý do "sleep để chờ điều kiện" trong vùng có lock gần như luôn là bug.
5. Daemon thread: người dọn dẹp bị bỏ lại
Mặc định, JVM chỉ exit khi mọi thread thường (non-daemon) đã kết thúc — main xong mà còn một worker đang chạy thì JVM vẫn sống chờ worker. Daemon thread là ngoại lệ: chúng là thread "phục vụ nền", và JVM không chờ chúng. Khi thread thường cuối cùng kết thúc, JVM exit ngay lập tức, bỏ rơi mọi daemon thread giữa chừng — không có exception, không có finally, dừng tại chỗ.
Thread cacheRefresher = new Thread(() -> {
while (true) {
refreshSeatMapCache();
try { Thread.sleep(60_000); } catch (InterruptedException e) { return; }
}
}, "cache-refresher");
cacheRefresher.setDaemon(true); // PHAI set truoc khi start()
cacheRefresher.start();
Ví dụ daemon nổi tiếng nhất chính là các thread của garbage collector: chúng tồn tại để phục vụ chương trình, và khi chương trình xong thì không còn lý do gì giữ JVM sống chỉ để... dọn rác.
Tiêu chí chọn: công việc mà việc bị cắt ngang không gây hậu quả — refresh cache, gửi metrics, dọn dẹp định kỳ — thì daemon phù hợp. Ngược lại là pitfall kinh điển: daemon thread đang ghi dở một file lúc JVM exit sẽ bị kill ngay giữa thao tác I/O — file cụt, buffer chưa flush, dữ liệu mất — và không một dòng log nào cho bạn biết. Bất kỳ việc gì phải hoàn thành trọn vẹn (ghi file, commit transaction, gửi response) đều phải chạy trên thread thường.
Lưu ý nhỏ: setDaemon(true) phải gọi trước start() — gọi sau ném IllegalThreadStateException, vì tính daemon được quyết định lúc OS thread được tạo.
6. Interrupt và cooperative cancellation
Đây là phần quan trọng nhất của bài — các bài sau về lock, blocking queue, executor, structured concurrency đều quay về đây mỗi khi chữ InterruptedException xuất hiện.
6.1 Vì sao không thể kill một thread
Câu hỏi tự nhiên: TicketFlow đang load sơ đồ ghế thì admin bấm shutdown — làm sao dừng thread seats-loader đang chạy giữa chừng?
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() từng tồn tại và đã bị deprecated từ Java 1.2 (và xóa hẳn ở Java 20+), vì 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ủa TicketFlow có thể để lại số vé đã trừ nhưng booking chưa ghi.
Thay vào đó, Java chọn mô hình cooperative cancellation — hủy có hợp tác: bên ngoài chỉ được phép yêu cầu dừng, còn thread đích tự quyết định khi nào và ở đâ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). Hai bên hợp tác: một bên gửi tín hiệu, một bên định kỳ kiểm tra tín hiệu và thoát có trật tự. Cơ chế gửi tín hiệu đó chính là interrupt.
6.2 interrupt() chỉ set một lá cờ
Mỗi thread mang theo 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 |
Điều cần khắc sâu: interrupt() không có quyền lực gì lên một thread không thèm kiểm tra cờ. Một vòng lặp tính toán thuần — không sleep, không wait, không check — sẽ chạy tiếp như chưa hề có cuộc gọi interrupt:
// 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ờ ở những chỗ dừng được an toàn. Tần suất là một trade-off: check mỗi vòng lặp thì phản hồi hủy nhanh; check thưa thì hủy trễ.
6.3 InterruptedException: khi blocking method nhìn cờ hộ bạn
Còn các thread đang ngủ thì sao — đang sleep(), join(), hay take() trên blocking queue (bài 11)? Chúng không chạy vòng lặp nào để check cờ. Vì thế các blocking method của JDK tự đảm nhận việc đó: khi thread đang block trong các method này mà bị interrupt, method ném InterruptedException để thread tỉnh dậy ngay, đồng thời — chi tiết hay bị bỏ qua — xóa luôn interrupt status.
Hai quyết định thiết kế ở đây đều có lý do:
Vì sao là checked exception? Vì "bị yêu cầu hủy giữa chừng" không phải bug — nó là một tình huống bình thường mà 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ó (dừng có trật tự? lan truyền lên trên?). Checked exception ép bạn viết chính sách đó ra thành code thay vì để compiler cho qua. Nó là 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ại thời điểm catch, tín hiệu interrupt đã được giao tận tay bạn dưới dạng exception — cờ đã hoàn thành nhiệm vụ đưa tin. Từ đây, quyền và trách nhiệm quyết định nằm ở code của bạn: hoặc kết thúc luôn, hoặc — nếu bạn không phải người có thẩm quyền quyết định — 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 tiếp theo dù bạn đã xử lý xong.
6.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: nếu method của bạn không đủ thẩm quyền quyết định "hủy thì làm gì" (thường là code thư viện, code tầng giữa), khai báo throws InterruptedException và để 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 bạn không thể throw (đang trong Runnable.run(), trong vòng lặp worker), xử lý tại chỗ và gọi Thread.currentThread().interrupt() để dựng lại lá cờ vừa bị xóa — nhờ đó vòng lặp ngoài, framework, hay bất kỳ ai kiểm tra cờ sau bạn vẫn biết có yêu cầu hủy:
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
SeatMap map = seatMapQueue.take();
process(map);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag -> vong while thay co va thoat
}
}
closeResources(); // diem thoat co trat tu
}
Và đây là pitfall phổ biến nhất toàn bộ Java concurrency — 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 khối catch này, 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 muốn shutdown một worker nuốt interrupt sẽ chờ mãi mãi. So với phiên bản đúng:
// DUNG — xu ly toi thieu van phai restore flag
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // giu tin hieu song cho tang tren
}
Một dòng khác biệt, nhưng là khác biệt giữa một hệ thống shutdown được trong 1 giây và một 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 — code phía trên không bao giờ biết cần dừng. Đây là quy tắc sẽ được nhắc lại ở các bài lock, blocking queue và executor.
7. Pitfall tổng hợp
❌ Gọi run() thay vì start() — code chạy đúng kết quả nhưng tuần tự trên thread hiện tại, không có concurrency nào xảy ra.
✅ Luôn start(). Nghi ngờ thì in Thread.currentThread().getName() để xem code thực sự chạy trên thread nào.
❌ start() hai lần trên cùng Thread object — IllegalThreadStateException, vì vòng đời thread một chiều.
✅ Mỗi lần chạy là một Thread mới, hoặc dùng thread pool.
❌ Dùng sleep() chờ điều kiện trong khi đang giữ lock — sleep không nhả lock, mọi thread khác kẹt BLOCKED.
✅ Cần chờ điều kiện thì dùng wait()/condition (bài 06, bài 08) — chúng nhả lock khi chờ.
❌ Đặt việc bắt buộc hoàn thành (ghi file, flush buffer) vào daemon thread — JVM exit là daemon bị cắt ngang không lời từ biệt. ✅ Daemon chỉ cho việc cắt ngang vô hại; việc phải trọn vẹn đi thread thường.
❌ Catch InterruptedException rồi bỏ trống — tín hiệu hủy bị nuốt, shutdown treo.
✅ Rethrow hoặc Thread.currentThread().interrupt().
❌ Tin rằng interrupt() dừng được mọi thread — vòng lặp CPU thuần không check cờ thì interrupt vô tác dụng.
✅ Tự cài isInterrupted() check ở các điểm dừng an toàn trong code tính toán dài.
8. Liên hệ các bài khác
- Bài 01 — Process và Thread: nền khái niệm của bài này — vì sao platform thread đắt (one-to-one với OS thread) giải thích vì sao
start()phải gọi xuống native. - Bài 03 — Thread safety: bài này tạo được nhiều thread chạy song song; bài 03 trả lời câu hỏi tiếp theo — chuyện gì xảy ra khi chúng cùng chạm vào một mảnh dữ liệu.
- Bài 06 — volatile & synchronized: mổ cơ chế monitor,
wait()/notify()— thứ màjoin()được xây bên trên, và lý doBLOCKEDtồn tại. - Bài 11 — Blocking queue và bài 13 — Executor:
take()némInterruptedExceptionvà executor shutdown bằng interrupt — cả hai dùng đúng pattern cooperative cancellation của section 6. - Bài 17 — 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.
9. 📚 Deep Dive
Spec / reference chính thức:
- Javadoc
java.lang.Thread(Java 21) — đọc kỹ phần mô tả interrupt status trêninterrupt(),isInterrupted(),interrupted()và enumThread.State. - JLS §17.2 — Wait Sets and Notification — đặc tả wait set, notification và tương tác với interrupt (§17.2.3) — nền hình thức của join/wait.
- Java Concurrency in Practice, chương 7 (Cancellation and Shutdown) — chương kinh điển về cancellation policy và pattern xử lý InterruptedException; section 6 của 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 đó.
10. Tóm tắt
new Thread(Runnable)+start()tạo OS thread mới rồi JVM invokerun()trên thread đó; gọi thẳngrun()chỉ là method call thường trên thread hiện tại.- Mỗi Thread object
start()được đúng một lần — lần hai némIllegalThreadStateException. - 6 trạng thái:
NEW→RUNNABLE→ (BLOCKED|WAITING|TIMED_WAITING) →TERMINATED;RUNNABLEgộp cả "đang chạy" và "chờ CPU". BLOCKED= chờ monitor lock;WAITING/TIMED_WAITING= chờ sự kiện (vô hạn / có hạn) — phân biệt này quan trọng khi đọc thread dump.join()chờ thread khác kết thúc, cài đặt bằng wait trên monitor của Thread object; ưu tiên bản có timeout trong production.sleep()đưa thread vàoTIMED_WAITINGnhưng không nhả lock đang giữ.- Daemon thread bị JVM bỏ rơi ngay khi thread thường cuối cùng kết thúc — chỉ dùng cho việc cắt ngang vô hại;
setDaemon(true)phải trướcstart(). - Java không kill thread:
interrupt()chỉ set cờ interrupt status — cooperative cancellation, thread đích tự chọn điểm dừng an toàn. - Blocking method (
sleep,join,wait,take...) phản ứng với interrupt bằng cách némInterruptedExceptionvà xóa cờ. - 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.
11. Tự kiểm tra
Q1Đoạn code tạo Thread t in tên thread hiện tại, rồi gọi t.run() từ main. Chương trình in ra gì, và vì sao không có lỗi nào báo cho bạn biết mình dùng sai?▸
Thread t in tên thread hiện tại, rồi gọi t.run() từ main. Chương trình in ra gì, và vì sao không có lỗi nào báo cho bạn biết mình dùng sai?In ra main — vì run() gọi trực tiếp chỉ là một method call bình thường, thực thi tuần tự trên chính thread đang gọi. Không có OS thread nào được tạo.
Không có lỗi vì về mặt ngôn ngữ, code hoàn toàn hợp lệ: run() là một public method như mọi method khác, và Runnable chạy xong vẫn cho kết quả đúng. Sai duy nhất là sai về concurrency — công việc chạy tuần tự thay vì song song — thứ mà compiler và runtime không có cách nào biết là ngoài ý muốn của bạn.
Chỉ start() mới gọi xuống native để tạo OS thread mới rồi nhờ JVM invoke run() trên thread đó. Cách kiểm tra nhanh khi nghi ngờ: in Thread.currentThread().getName() bên trong công việc.
Q2Vì 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)?▸
Vì interrupt() không có quyền lực cưỡng chế nào — nó chỉ set một lá cờ (interrupt status) trên thread đích. Cờ chỉ có tác dụng ở hai nơi: (1) code tự kiểm tra nó bằng isInterrupted(), hoặc (2) các blocking method của JDK kiểm tra hộ và ném InterruptedException.
Vòng lặp CPU thuần không rơi vào trường hợp nào cả: nó không bao giờ block nên không có blocking 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 — vòng lặp chạy tiếp như chưa có gì xảy ra.
Đây là hệ quả trực tiếp của thiết kế cooperative cancellation: Java từ chối kill thread tại điểm tùy ý (vì sẽ vỡ invariant đang sửa dở), nên trách nhiệm cài "điểm dừng an toàn" — check cờ ở mỗi vòng lặp hay mỗi đơn vị công việc — thuộc về người viết code.
Q3sleep() có nhả lock không? Hệ quả là gì khi sleep bên trong vùng synchronized?▸
Không. sleep() chỉ đưa thread hiện tại vào TIMED_WAITING — nó không biết và không quan tâm thread đang giữ lock nào, nên mọi monitor lock vẫn nằm nguyên trong tay thread đang ngủ.
Hệ quả: thread khác cần vào vùng synchronized cùng lock sẽ đứng BLOCKED suốt giấc ngủ đó — một sleep(5000) trong vùng có lock là 5 giây cả hệ thống xếp hàng. Đây là lý do "sleep để chờ điều kiện" trong vùng có lock gần như luôn là bug.
Công cụ đúng cho "chờ điều kiện" là wait() — nó nhả lock trước khi ngủ và lấy lại lock khi tỉnh dậy (cơ chế ở bài 06). Ghi nhớ nhanh: sleep ôm lock ngủ, wait gửi lock lại rồi mới ngủ.
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 tồn tại dưới dạng exception trong tay bạn. Catch rỗng vứt exception đi mà không làm gì: cờ đã xóa, exception đã nuốt, tín hiệu hủy biến mất hoàn toàn khỏi hệ thống.
Hậu quả thực tế: vòng lặp worker tiếp tục chạy như thường, thread pool gọi shutdown chờ mãi không thấy worker thoát, ứng dụng phải kill cứng. Bug này âm thầm vì code vẫn "chạy đúng" cho đến ngày cần dừng.
Hai cách hợp lệ: (1) rethrow — khai báo throws InterruptedException đẩy quyết định lên caller, dùng khi method của bạn 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 còn nhìn thấy yêu cầu dừng.
Q5Thread dump cho thấy 40 thread BLOCKED và 40 thread WAITING. Hai nhóm này đang gặp chuyện gì khác nhau?▸
BLOCKED có đúng một nghĩa: thread muốn vào vùng synchronized nhưng monitor lock đang trong tay thread khác. 40 thread BLOCKED nghĩa là hệ thống đang tranh lock — nhiều khả năng có một lock nóng (hoặc một thread ôm lock quá lâu) tạo nút cổ chai; hướng điều tra là tìm thread đang giữ lock đó làm gì.
WAITING là chờ một sự kiện: thread khác kết thúc (join), một notify, một unpark. 40 thread WAITING thường là worker đang chờ việc (vd chờ phần tử trong queue) — có thể hoàn toàn bình thường, hoặc là dấu hiệu upstream không đẩy việc xuống.
Cùng là "đứng yên" nhưng hai bệnh khác nhau: BLOCKED là bệnh tranh chấp (contention), chữa bằng thu hẹp vùng lock hoặc đổi cấu trúc dữ liệu; WAITING là chuyện luồng công việc, chữa ở chỗ khác. Đọc được phân biệt này là kỹ năng chẩn đoán thread dump cơ bản nhất.
Q6Vì sao JVM exit khi chỉ còn daemon thread? Nêu một tình huống daemon thread gây mất dữ liệu.▸
Daemon thread theo định nghĩa là thread phục vụ — tồn tại để hỗ trợ các thread thường (như GC thread dọn rác hộ chương trình). Khi mọi thread thường đã kết thúc, "chương trình" theo nghĩa người dùng đã xong; giữ JVM sống chỉ để các thread phục vụ tiếp tục phục vụ... không ai cả là vô nghĩa. Nên JVM exit ngay, không chờ.
Cái giá: daemon bị dừng tại chỗ, ngay lập tức — không exception, không chạy finally, không cơ hội dọn dẹp. Tình huống mất dữ liệu kinh điển: daemon thread định kỳ ghi log hoặc flush buffer xuống file; JVM exit đúng lúc nó đang ghi dở — file cụt, phần buffer chưa flush mất vĩnh viễn, và không có dấu vết nào để debug.
Quy tắc chọn: việc bị cắt ngang vô hại (refresh cache, gửi metrics) → daemon được; việc phải hoàn thành trọn vẹn (I/O ghi, commit, gửi response) → bắt buộc thread thường.
Q7Gọi start() lần thứ hai trên một Thread đã chạy xong thì điều gì xảy ra? Vì sao Java thiết kế như vậy thay vì cho thread "chạy lại"?▸
start() lần thứ hai trên một Thread đã chạy xong thì điều gì xảy ra? Vì sao Java thiết kế như vậy thay vì cho thread "chạy lại"?Ném IllegalThreadStateException ngay lập tức — kể cả khi thread đã TERMINATED từ lâu và "rảnh".
Vì một Thread object đại diện cho một lần thực thi với vòng đời một chiều: NEW → RUNNABLE → ... → TERMINATED, không có cạnh quay ngược. Cho phép restart nghĩa là phải định nghĩa lại toàn bộ trạng thái cũ (interrupt status còn không? ai đang join nó thì sao? stack cũ đi đâu?) — mơ hồ và dễ sinh bug hơn nhiều so với quy tắc đơn giản "mỗi object một lần chạy".
Nhu cầu "chạy lại công việc" có lời giải đúng hơn: tách công việc (Runnable — tạo lại bao nhiêu lần cũng được) khỏi thread, rồi để thread pool tái sử dụng một tập thread cho nhiều công việc — chính là hướng đi của bài 13 (Executor).
Bài tiếp theo: Thread safety — khi nhiều thread cùng chạm vào dữ liệu
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