Thread cơ bản — chạy song song, start/join/interrupt
Thread và Runnable, lifecycle 6 trạng thái, start vs run, join, interrupt cooperative. Vì sao single-thread không đủ khi gặp I/O, thread sinh ra từ đâu trong JVM, và tại sao Thread.stop() bị deprecated.
Bạn viết 1 script crawl 100 URL. Mỗi URL gọi HTTP mất ~500ms — thời gian chờ network round-trip. Code bình thường:
for (String url : urls) {
String body = httpGet(url); // Cho 500ms
save(body);
}
Tổng thời gian: 100 × 500ms = 50 giây. Trong 50 giây đó, CPU của bạn làm gì? Gần như không làm gì — 99% thời gian chỉ ngồi chờ packet TCP về. Máy 8 core, 16 luồng phần cứng, nhưng chương trình chỉ dùng 1.
Đây là vấn đề ai cũng gặp: task bị chặn bởi I/O (network, disk, DB) — CPU rảnh nhưng chương trình vẫn chậm. Giải pháp: tạo thread — đơn vị thực thi nhỏ hơn process, nhiều thread chạy song song, thread nào đang chờ I/O thì thread khác tận dụng CPU.
Bài này giải thích thread là gì ở cấp OS và JVM, cách tạo thread trong Java (3 cách), sự khác biệt start() vs run() — lỗi kinh điển của người mới, lifecycle 6 trạng thái, và cách dừng thread cooperative qua interrupt (không phải stop() — method đó deprecated có lý do).
1. Analogy — Bếp nhà hàng một và nhiều đầu bếp
Nhà hàng chỉ có 1 đầu bếp: khách đầu order xong, khách thứ hai phải chờ đến khi món đầu xong mới bắt đầu. Nếu món đầu cần ninh xương 30 phút, đầu bếp đứng canh nồi → khách 2 chờ 30 phút dù nguyên liệu món họ sẵn.
Thuê thêm 4 đầu bếp phó: mỗi người nhận 1 order riêng. Đầu bếp 1 đang đứng canh nồi thì đầu bếp 2 chiên, đầu bếp 3 trộn salad, đầu bếp 4 nướng bánh. Nhà bếp bận rộn cùng lúc. Khi đầu bếp 1 canh nồi, OS "đầu bếp trưởng" điều phối — ai đang rảnh thì giao việc.
Quan trọng: cả 5 đầu bếp dùng chung tủ lạnh (heap). Nhưng mỗi người có bộ dao/thớt riêng (stack) — không lẫn nhau. Đây là thiết kế của thread.
| Đời thường | JVM |
|---|---|
| Đầu bếp | Thread |
| Tủ lạnh chung (nguyên liệu, gia vị) | Heap (shared memory — object, array) |
| Bộ dao/thớt riêng | Stack (local variable, call frame) |
| Đầu bếp trưởng phân công | OS scheduler (quyết định thread nào chạy core nào) |
| Đầu bếp đứng canh nồi | Thread đang sleep / block I/O |
| Chỉ 1 đầu bếp được mở tủ gia vị tại 1 lúc | synchronized lock (bài kế tiếp) |
Điểm cần khắc sâu: heap share là vừa sức mạnh vừa là cội nguồn mọi bug concurrency. Sức mạnh vì thread A sửa object, thread B thấy ngay — không cần copy, không cần IPC. Cội nguồn bug vì thread A sửa giữa chừng object, thread B đọc thấy trạng thái nửa vời. Bài 10.2 (synchronized + volatile) giải quyết chính xác vấn đề này.
Thread = luồng thực thi. Stack riêng mỗi thread (local var không xung đột), heap chung tất cả thread (object xung đột được — cần đồng bộ).
2. Vì sao cần thread — chi tiết với số liệu
Quay lại ví dụ crawler. Giả định:
- Mỗi HTTP call: 10ms CPU compute (parse response) + 490ms chờ network.
- 100 URL.
Single thread:
- Tổng CPU: 100 × 10ms = 1000ms = 1 giây.
- Tổng chờ: 100 × 490ms = 49 giây.
- Walltime: ~50 giây.
10 thread cùng crawl:
- Mỗi thread crawl 10 URL tuần tự. Tổng CPU per thread: 100ms. Tổng chờ per thread: 4900ms.
- 10 thread chạy song song trên 8 core CPU — OS scheduler chia đều.
- Walltime: ~5 giây (10× nhanh hơn).
CPU tổng vẫn 1 giây, nhưng giờ phân bố đều qua 5 giây thay vì bị "giãn" qua 50 giây. Đây là tăng throughput nhờ parallelism che I/O wait.
Lưu ý giới hạn: nếu task là CPU-bound (tính toán thuần, không I/O), số thread tối ưu = số CPU core. Thêm nữa chỉ tăng overhead (context switch). Bài 10.5 (virtual thread) giải thích khi nào scale được lên hàng nghìn thread.
3. Tạo thread — 3 cách và khi nào dùng cái nào
Cách 1: Runnable + Thread (learning, low-level)
Runnable là functional interface có đúng 1 method void run(). Nó biểu thị "một hành động chạy được, không return value".
Runnable task = () -> {
System.out.println("Hello from " + Thread.currentThread().getName());
};
Thread t = new Thread(task, "worker-1");
t.start(); // Khoi tao OS thread moi, thread do goi task.run()
t.join(); // Main thread cho t xong moi di tiep
Điểm cần hiểu:
- Lambda
() -> { ... }tự động được compiler map sangRunnablevìRunnablecó đúng 1 abstract method. Không cầnnew Runnable() { public void run() {...} }verbose. - Constructor
Thread(Runnable, name):namegiúp debug — khi xem log hoặc stack trace, thấy tên "worker-1" dễ trace hơn "Thread-3". start()là điểm tách luồng. Sau dòng này, có 2 luồng thực thi song song: main thread tiếp tục dòng saustart(), thread mới chạyrun().
Cách này dùng khi học hoặc cần control chi tiết từng thread (hiếm). Production không ai tạo new Thread() — tốn, không tái dùng.
Cách 2: Extends Thread (anti-pattern, tránh)
class Worker extends Thread {
@Override
public void run() {
System.out.println("Hi from worker");
}
}
new Worker().start();
Cách này không nên dùng. 3 lý do:
-
Single inheritance: Java không cho phép kế thừa nhiều class. Kế thừa
Thread→ class của bạn mất slot kế thừa — không extend đượcBaseService,AbstractRepositoryhay bất kỳ class nào khác. -
Coupling logic với cơ chế: logic task (cái bạn muốn làm) gắn chặt với cơ chế thread. Muốn chạy cùng task trên
ExecutorServicepool thay vì thread riêng — phải refactor. Nếu làRunnable, task vẫn là task, muốn chạy ở đâu tuỳ bạn. -
Composition over inheritance (nguyên tắc module 6):
Runnablelà interface "thứ chạy được". Class của bạn có một hành động chạy được (has-a) — không phải là một thread (is-a). Ngữ nghĩa đúng.
Khi nào thực sự cần extends Thread? Gần như không bao giờ — trừ khi viết framework thread riêng.
Cách 3: ExecutorService (best practice production)
ExecutorService exec = Executors.newFixedThreadPool(4);
exec.submit(() -> System.out.println("task 1"));
exec.submit(() -> System.out.println("task 2"));
exec.submit(() -> System.out.println("task 3"));
exec.shutdown();
ExecutorService là thread pool — nhóm thread sẵn có, tái dùng qua nhiều task. Pool 4 thread xử lý 1000 task: mỗi thread làm task xong, pool cho nó task mới, không tạo thread mới.
Vì sao production luôn dùng pool:
- Tạo thread tốn (sẽ giải thích kỹ ở mục 8): ~1MB memory + syscall OS. 1000 task → 1000 thread mới = 1GB memory + 1000 syscall.
- Limit tài nguyên: pool size = 10 nghĩa là tối đa 10 task chạy song song. Nếu submit 1000 task, 990 task xếp hàng chờ. Bảo vệ hệ thống khỏi traffic spike.
- Lifecycle rõ ràng:
shutdown()dừng nhận task mới;awaitTermination(timeout)chờ task đang chạy xong. Không rò thread.
Chi tiết ExecutorService ở bài 10.3. Từ giờ, mọi ví dụ "chạy task song song" trong các bài concurrency sẽ dùng pool.
4. start() vs run() — nhầm lẫn của 99% người mới
Đây là bẫy kinh điển. Code sau compile pass, chạy pass, nhưng sai hoàn toàn:
Runnable task = () -> System.out.println(Thread.currentThread().getName());
Thread t = new Thread(task, "worker-1");
t.run(); // (1)
t.start(); // (2)
Output:
main
worker-1
Giải thích từng dòng:
- (1)
t.run(): gọi methodrun()như method bình thường. Không có thread mới. Lambda body chạy trên thread đang gọi — tức là main thread. Vì thếThread.currentThread().getName()trảmain. - (2)
t.start(): bảo JVM tạo OS thread mới. JVM gọi native code (pthread_create trên Linux, CreateThread trên Windows), thread mới bắt đầu chạyrun()độc lập. Trên thread mới,Thread.currentThread()làt, nên tên "worker-1".
Vì sao thiết kế cho phép gọi run() như method thường? Vì Thread implement Runnable — nó là một Runnable. Gọi run() hợp lệ cú pháp, compiler không chặn.
Hậu quả nhầm lẫn trong thực tế: lập trình viên viết t.run() tưởng mình đã parallel. Task chạy sequential, nhưng không crash, không warning — chỉ là chậm không đạt kỳ vọng. Bug latent, khó phát hiện nếu không đo walltime.
run() gọi method. start() tạo thread. Muốn parallel — start(). Gọi run() của Thread trong code production gần như luôn là bug.
Cách phát hiện nhanh: trong task, in Thread.currentThread().getName(). Nếu thấy "main" ở chỗ bạn kỳ vọng "worker-N" → gọi nhầm run().
5. Thread lifecycle — 6 trạng thái và chuyển tiếp
Thread trong Java có 6 trạng thái định nghĩa trong enum Thread.State. Hiểu lifecycle giúp debug khi thấy thread "đứng" (stuck) — biết nó stuck ở state nào thì đoán được nguyên nhân.
stateDiagram-v2
[*] --> NEW: new Thread(...)
NEW --> RUNNABLE: start()
RUNNABLE --> BLOCKED: cho synchronized lock
RUNNABLE --> WAITING: wait() / join() khong timeout
RUNNABLE --> TIMED_WAITING: sleep(ms) / wait(ms) / join(ms)
BLOCKED --> RUNNABLE: nhan duoc lock
WAITING --> RUNNABLE: notify / target thread terminate
TIMED_WAITING --> RUNNABLE: timeout hoac notify
RUNNABLE --> TERMINATED: run() return / exception
TERMINATED --> [*]Chi tiết từng state:
NEW
Thread object đã tạo nhưng chưa start(). Tồn tại như Java object, nhưng chưa có OS thread nào được cấp. State này chỉ tồn tại ngắn ngủi giữa new Thread(...) và start().
RUNNABLE
"Có thể chạy" — đang thực sự chạy trên core CPU hoặc đang xếp hàng chờ OS scheduler cho slot. Java không phân biệt "đang chạy" và "chờ CPU" — cả hai đều RUNNABLE. Điều này hơi khác với thuật ngữ OS (OS phân biệt RUNNING và READY).
Ý nghĩa thực tế: thấy thread ở RUNNABLE — nó không bị block, đang làm việc. Nếu walltime dài mà vẫn RUNNABLE, có thể là CPU-bound (tính toán nặng) hoặc loop vô hạn.
BLOCKED
Thread đang chờ vào synchronized block. Có thread khác đang giữ monitor. Khác với WAITING ở chỗ: BLOCKED luôn là "chờ lock vào block synchronized".
Ý nghĩa thực tế: thấy nhiều thread BLOCKED cùng 1 lock → có contention cao, cần xem xét design. Dùng jstack (bài 12 về JVM internals) để lấy thread dump, thấy stack trace → biết đang chờ lock nào.
WAITING
Chờ không có timeout. Gây bởi:
Object.wait()— chờ notify.thread.join()— chờ thread target terminate.LockSupport.park()— chờ unpark.
Thread ở WAITING không tốn CPU. Ngủ đông cho đến khi có tín hiệu.
TIMED_WAITING
Giống WAITING nhưng có timeout. Gây bởi:
Thread.sleep(ms).Object.wait(ms).thread.join(ms).LockSupport.parkNanos(ns).
Sau timeout, thread tự trở lại RUNNABLE — không cần ai notify.
TERMINATED
run() đã return (bình thường hoặc do exception không catch). Thread không chạy lại được — start() lần 2 throw IllegalThreadStateException. Thread object vẫn tồn tại (GC sẽ dọn) nhưng "chết".
Truy vấn state runtime:
Thread t = new Thread(...);
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (hoac da TERMINATED neu task xong nhanh)
Ứng dụng: viết debug tool, health check, hoặc đơn giản là jstack output show state mỗi thread — đọc được là tự debug được.
6. join() — chờ thread khác hoàn thành
Thread worker = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Worker done");
});
worker.start();
worker.join(); // Main BLOCK o day 1 giay
System.out.println("Main continue");
Output:
Worker done
Main continue
Không có join(): main thread có thể return trước worker — program exit, worker bị kill dở. Với join(): main đứng tại dòng đó cho đến khi worker ở state TERMINATED.
Bên dưới join() dùng cơ chế gì? Đơn giản là chờ tín hiệu: khi thread X kết thúc, JVM notify tất cả thread đang join() thread X. Dựa trên rule happens-before (bài 10.2): mọi action trong thread X happens-before x.join() return — nghĩa là main sau join thấy mọi update worker đã làm, không cần volatile hay synchronized thủ công.
Ví dụ tận dụng happens-before:
int[] result = new int[1];
Thread compute = new Thread(() -> {
result[0] = heavyCompute();
});
compute.start();
compute.join();
System.out.println(result[0]); // Dam bao thay ket qua, khong can dong bo khac
Biến thể có timeout: worker.join(5000) — chờ tối đa 5 giây. Hết timeout thì return (worker có thể vẫn chạy). Check worker.isAlive() để biết worker đã xong chưa.
7. interrupt() — cơ chế dừng thread cooperative
Vấn đề: làm sao dừng 1 thread đang chạy? Bản năng: thread.stop(). Tuyệt đối không. Method này đã deprecated từ Java 1.2 (1998) và có lý do.
Vì sao stop() nguy hiểm
Thread.stop() force-kill thread tại bất kỳ instruction nào. Vấn đề: thread có thể đang giữ lock, đang update object giữa chừng, đang giữ resource. Bị kill:
- Lock không được release → thread khác chờ mãi → deadlock.
- Object bị update nửa vời → state không nhất quán → bug kỳ lạ ở thread khác.
- Resource (file, socket) không được close → leak.
Thiết kế an toàn: cơ chế cooperative — main thread xin worker dừng, worker chủ động check và dừng tại điểm an toàn.
Cơ chế flag
Mỗi thread có flag boolean interrupted. thread.interrupt() set flag này. Thread worker check flag (Thread.interrupted() hoặc isInterrupted()) và quyết định dừng.
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
doWork();
Thread.sleep(100);
} catch (InterruptedException e) {
// sleep bi interrupt - flag da bi clear, phai restore
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Worker stopped cleanly");
// Cleanup o day: close file, release resource
});
worker.start();
Thread.sleep(500);
worker.interrupt(); // Request stop
worker.join();
Chi tiết: InterruptedException và clear flag
Đoạn xử lý InterruptedException nhìn lạ. Giải thích:
Khi thread đang trong sleep/wait/join và bị interrupt, JVM:
- Throw
InterruptedException. - Clear flag interrupt (set về false).
Vì sao clear? Vì API muốn bạn chủ động quyết định: bạn đã biết thread bị interrupt (qua exception), giờ bạn tự xử lý — tiếp tục hay dừng. Nếu flag vẫn true, code sau catch while (isInterrupted()) sẽ thấy true và xử lý lần nữa — double-counting.
Nhưng điều này tạo ra bẫy: nếu bạn catch xong return hoặc bình thản tiếp tục, thread mất tín hiệu dừng. Loop ngoài check isInterrupted() không thấy → tiếp tục chạy.
Pattern chuẩn: sau catch, restore flag bằng Thread.currentThread().interrupt() — set lại true. Lần loop sau while (!isInterrupted()) sẽ thấy true và exit.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // RESTORE
break; // hoac return / throw
}
Đây là idiom Java phải thuộc. Bỏ qua Thread.currentThread().interrupt() là bug kinh điển — CI pass, production thread không dừng được → memory leak dần.
catch (InterruptedException e) { } — im lặng nuốt exception — là bug. Tín hiệu dừng mất. Nếu bạn thực sự muốn bỏ qua interrupt (rất hiếm), ít nhất log e để biết lý do. Thường thì restore flag.
Method có tôn trọng interrupt
Không phải mọi method đều check interrupt. Danh sách method respond đến interrupt (throw InterruptedException):
Thread.sleep(),Object.wait(),Thread.join().BlockingQueue.take(),BlockingQueue.put().Lock.lockInterruptibly()(không phảilock()thường).Socketread/write nếu channel đăng ký interrupt.
Method không respond:
- I/O cổ điển
InputStream.read()— block đến khi có data, interrupt không được. Lock.lock()(không phảilockInterruptibly).- Loop tính toán thuần — phải tự check
isInterrupted().
Đây là lý do interrupt được gọi "cooperative" — cần code phối hợp. stop() là force, nguy hiểm; interrupt() là request, an toàn.
8. Thread ở cấp OS — chi phí thực sự
Chạy Thread.start() bên dưới làm gì? Tóm tắt:
- JVM gọi OS API native:
pthread_create(Linux/macOS),CreateThread(Windows). - OS cấp phát stack cho thread — mặc định 1MB (Linux default
ulimit -s). - OS đăng ký thread với scheduler — thread xuất hiện trong
/proc/<pid>/task(Linux) hoặc Task Manager (Windows). - JVM set thread state RUNNABLE, OS scheduler có thể dispatch.
Con số quan trọng để có mental model:
| Operation | Thời gian |
|---|---|
| Tạo thread (syscall + mem alloc) | ~100 microsecond |
| Context switch (OS đổi thread trên 1 core) | ~1-10 microsecond |
| Memory mỗi thread (stack default) | ~1 MB |
Hệ quả:
- 10k thread = 10GB memory. Laptop 16GB RAM chết — OS bắt đầu swap, performance sập.
- Tạo 1000 thread/s = 100ms riêng cho syscall. Đáng kể cho latency-sensitive app.
- Context switch giữa thread tốn CPU cache — data trong L1/L2 của thread A bị evict khi switch sang B.
Đây là cơ sở cho 2 design pattern:
- Thread pool (bài 10.3): tạo 1 lần, tái dùng nhiều task — amortize cost.
- Virtual thread Java 21 (bài 10.5): thread user-space ~300 byte — tạo triệu thread được.
9. Daemon thread — thread background
Thread Java có 2 loại theo "nặng cân" trong lifecycle JVM:
- Non-daemon (mặc định): JVM đợi hết trước khi exit. Program chỉ kết thúc khi tất cả non-daemon terminate.
- Daemon: JVM không đợi. Khi tất cả non-daemon kết thúc, JVM exit luôn, daemon bị kill (không có cleanup).
Thread t = new Thread(() -> {
while (true) {
logMetrics();
try { Thread.sleep(1000); } catch (InterruptedException e) { return; }
}
});
t.setDaemon(true); // Phai set TRUOC start()
t.start();
Sau khi main kết thúc, t vẫn đang loop — nhưng vì là daemon, JVM không chờ. Program exit ngay.
Khi nào dùng daemon:
- Background task không critical: metrics logger, heartbeat, cache warmer, GC helper. Mất vài entry log không chết ai.
- Thread utility: reaper, watchdog.
Khi không dùng daemon:
- Task có data phải flush: DB write, file write. JVM exit giữa chừng → mất data. Phải là non-daemon + shutdown hook để graceful.
t.start(); t.setDaemon(true); → IllegalThreadStateException. Lý do: daemon flag là thuộc tính OS thread — OS cần biết trước khi thread được đăng ký scheduler.
10. Pitfall tổng hợp
❌ Nhầm 1: Gọi run() thay start().
thread.run(); // Chay tren main thread, khong parallel
✅ thread.start() cho parallel. Check bằng Thread.currentThread().getName().
❌ Nhầm 2: Nuốt InterruptedException.
try { Thread.sleep(1000); }
catch (InterruptedException e) { } // Mat tin hieu dung
✅ Restore flag để outer loop thấy:
try { Thread.sleep(1000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return; // hoac break / throw
}
❌ Nhầm 3: Dùng Thread.stop().
thread.stop(); // Deprecated, force-kill, leak lock/resource
✅ thread.interrupt() + thread tự check isInterrupted() ở điểm an toàn.
❌ Nhầm 4: Extends Thread.
class MyTask extends Thread { ... }
✅ Implement Runnable (lambda thường đủ) + dùng ExecutorService.
❌ Nhầm 5: setDaemon(true) sau start().
t.start();
t.setDaemon(true); // IllegalThreadStateException
✅ Set trước start(), hoặc pass qua factory khi dùng ExecutorService.
❌ Nhầm 6: Gọi start() 2 lần.
thread.start();
thread.start(); // IllegalThreadStateException - thread da chay
✅ 1 Thread object chạy 1 lần. Cần "chạy lại" → tạo thread mới hoặc dùng pool.
11. 📚 Deep Dive Oracle
Spec / reference chính thức:
- Thread class javadoc — API đầy đủ, lifecycle diagram.
- Thread.State enum — 6 state định nghĩa chính xác.
- JLS §17.1 Synchronization — memory model (đọc kỹ trước khi vào bài 10.2).
- Oracle tutorial: Concurrency — hands-on, dễ tiếp cận hơn JLS.
- JEP 444: Virtual Threads — Java 21 virtual thread, giải thích vì sao platform thread không scale.
Ghi chú: JLS §17 là tài liệu khó nhất trong spec Java — nhiều thuật ngữ formal (happens-before, synchronization order). Với người mới, đọc Oracle tutorial trước để có intuition, sau đó JLS để nắm chính xác. JEP 444 cho bức tranh tại sao Java 21 thay đổi — đọc sau khi hiểu platform thread (bài này).
12. Tóm tắt
- Thread = luồng thực thi độc lập. Stack riêng mỗi thread, heap chung. Shared heap là nguồn sức mạnh và nguồn mọi bug concurrency.
- Cần thread khi task I/O-bound — CPU rảnh trong lúc chờ network/disk, dùng thread khác để tận dụng.
- 3 cách tạo:
Runnable+new Thread()(học), extendsThread(tránh),ExecutorService(production). start()tạo OS thread mới và chạyrun()trên đó; gọirun()trực tiếp không tạo thread — bẫy kinh điển.- 6 state: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. Hiểu state giúp debug thread dump.
join()chờ thread target terminate. Tận dụng happens-before — main sau join thấy mọi update worker.Thread.stop()deprecated — force-kill không an toàn.interrupt()cooperative — set flag, thread tự check.InterruptedExceptionthrow + clear flag — pattern phải restore bằngThread.currentThread().interrupt().- Platform thread nặng (~1MB stack, ~100μs tạo) — không scale lên 10k thread. Bài 10.5 giới thiệu virtual thread giải quyết.
- Daemon thread không giữ JVM sống — set
setDaemon(true)trướcstart(). Dùng cho background non-critical.
13. Tự kiểm tra
Q1Khác biệt cụ thể giữa thread.run() và thread.start() là gì?▸
thread.run() và thread.start() là gì?start(): JVM gọi OS native (pthread_create / CreateThread) tạo OS thread mới, đăng ký với scheduler, rồi thread mới gọi run(). Main thread tiếp tục dòng sau start() song song.
run(): gọi method như method Java bình thường. Không có thread mới, không syscall. Lambda body chạy tuần tự trên thread của caller — thường là main thread.
Cách phát hiện nhầm lẫn trong code: in Thread.currentThread().getName() trong task. Thấy "main" khi kỳ vọng "worker-N" → gọi nhầm run().
Quy tắc: muốn parallel → start(). Gọi run() của Thread trong code production gần như luôn là bug.
Q2Vì sao InterruptedException lại clear interrupt flag khi throw, và vì sao phải restore nó?▸
Clear flag là thiết kế để trao quyền quyết định cho handler: thread đã biết bị interrupt (qua exception), giờ bạn chọn dừng hay tiếp tục. Nếu flag vẫn true, code while (isInterrupted()) sau đó sẽ lại thấy true và xử lý lần 2 — double-counting.
Tuy nhiên, phần lớn trường hợp bạn muốn tín hiệu dừng lan truyền: nuốt exception xong, loop bên ngoài check isInterrupted() phải thấy true → exit. Nếu không restore, loop không dừng — thread tiếp tục chạy, mất hoàn toàn tín hiệu interrupt.
Pattern chuẩn:
try { Thread.sleep(1000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag
break; // hoac return / throw
}Rule: luôn restore (hoặc throw tiếp InterruptedException nếu method caller declare). Không bao giờ catch { } rỗng.
Q3Đoạn sau in gì? Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()), "worker"); t.run(); t.start();▸
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()), "worker"); t.run(); t.start();t.run() gọi method Java thường — lambda chạy trên thread đang gọi (main). In "main".
t.start() tạo OS thread mới tên "worker", thread đó chạy run() → in "worker".
main
workerThứ tự: "main" luôn in trước (đồng bộ, chạy ngay ở main). "worker" in sau — có thể ngay lập tức hoặc trễ vài ms tuỳ OS scheduler.
Lưu ý: Thread cho phép gọi start() sau khi đã gọi run() vì run() không đổi state thread sang RUNNABLE — run() không biết nó đang được gọi như method hay được start thật.
Q4Vì sao trong code production không nên dùng extends Thread?▸
extends Thread?3 lý do:
- Single inheritance: Java không cho kế thừa nhiều class. Extends
Thread→ class của bạn mất slot kế thừa; không extend đượcBaseService,AbstractRepository, hay framework base class khác. - Coupling logic với cơ chế: logic task gắn chặt với lifecycle thread. Muốn chạy task trên
ExecutorServicepool thay vì thread dedicated — phải refactor. Nếu tách thànhRunnable, task là task, cơ chế chạy tách biệt. - Composition over inheritance (module 6):
Runnablebiểu thị "thứ chạy được". Class của bạn có một hành động chạy được (has-a), không phải là một thread (is-a). Semantic đúng.
Pattern đúng: implement Runnable hoặc dùng lambda, pass vào Thread hoặc ExecutorService. 95% thời gian dùng ExecutorService, không tự tạo Thread.
Q5Vì sao setDaemon(true) phải gọi trước start()? Và khi nào nên dùng daemon?▸
Daemon flag là thuộc tính OS thread — được set khi thread đăng ký với scheduler. Sau start(), OS thread đã tồn tại, flag không đổi được → IllegalThreadStateException.
Khi nào dùng daemon:
- Background task không critical: metrics logger, heartbeat, cache warmer. Mất vài entry cuối khi JVM exit không chết ai.
- Utility thread: reaper (clean up orphan resource), watchdog (monitor other thread).
Khi không dùng daemon:
- Task có data phải flush: DB write, file write, audit log. JVM exit giữa chừng → mất data không recoverable. Phải là non-daemon + shutdown hook để flush graceful.
Quy tắc: daemon = "JVM có thể kill tôi bất kỳ lúc nào". Không dùng cho task cần bảo đảm completion.
Q6Giả sử thread A gọi t.join() và thread B là t vừa kết thúc. Thread A có thấy được biến t đã update trong run() không, mà không cần volatile?▸
t.join() và thread B là t vừa kết thúc. Thread A có thấy được biến t đã update trong run() không, mà không cần volatile?Có — đảm bảo bởi rule happens-before của Java Memory Model (bài 10.2): "Mọi action trong thread T happens-before t.join() return ở thread khác".
Nghĩa là: khi join() return, mọi ghi mà thread T đã làm (kể cả biến không phải volatile, không qua synchronized) đều visible cho thread gọi join. Đây là memory barrier ngầm.
int[] result = new int[1];
Thread t = new Thread(() -> {
result[0] = heavyCompute(); // Ghi khong co synchronization
});
t.start();
t.join();
System.out.println(result[0]); // Dam bao thay ket quaĐây là lý do join() là cơ chế đồng bộ đơn giản và đúng cho pattern "main tạo worker, worker tính, main lấy kết quả" — không cần volatile, không cần synchronized, không cần AtomicReference.
Cảnh báo: chỉ áp dụng cho thread bạn join. Thread khác không gọi join() không có guarantee này.
Bài tiếp theo: Synchronized và volatile — memory model và happens-before
Bài này có giúp bạn hiểu bản chất không?