Java — Từ Zero đến Senior/Thread cơ bản — chạy song song, start/join/interrupt
~22 phútConcurrency cơ bảnMiễn phí

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ườngJVM
Đầu bếpThread
Tủ lạnh chung (nguyên liệu, gia vị)Heap (shared memory — object, array)
Bộ dao/thớt riêngStack (local variable, call frame)
Đầu bếp trưởng phân côngOS scheduler (quyết định thread nào chạy core nào)
Đầu bếp đứng canh nồiThread đang sleep / block I/O
Chỉ 1 đầu bếp được mở tủ gia vị tại 1 lúcsynchronized 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.

💡 Cách nhớ

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)

Runnablefunctional 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 sang RunnableRunnable có đúng 1 abstract method. Không cần new Runnable() { public void run() {...} } verbose.
  • Constructor Thread(Runnable, name): name giú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 sau start(), thread mới chạy run().

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:

  1. 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 được BaseService, AbstractRepository hay bất kỳ class nào khác.

  2. 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 ExecutorService pool 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.

  3. Composition over inheritance (nguyên tắc module 6): Runnable là 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();

ExecutorServicethread 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 method run() 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ạy run() độc lập. Trên thread mới, Thread.currentThread()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ó 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.

⚠️ Quy tắc 1 câu

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(...)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:

  1. Throw InterruptedException.
  2. 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.

⚠️ Đừng nuốt InterruptedException

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ải lock() thường).
  • Socket read/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ải lockInterruptibly).
  • 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:

  1. JVM gọi OS API native: pthread_create (Linux/macOS), CreateThread (Windows).
  2. OS cấp phát stack cho thread — mặc định 1MB (Linux default ulimit -s).
  3. OS đăng ký thread với scheduler — thread xuất hiện trong /proc/<pid>/task (Linux) hoặc Task Manager (Windows).
  4. JVM set thread state RUNNABLE, OS scheduler có thể dispatch.

Con số quan trọng để có mental model:

OperationThờ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:

  1. Thread pool (bài 10.3): tạo 1 lần, tái dùng nhiều task — amortize cost.
  2. 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.
⚠️ setDaemon phải trước start()

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

📚 Deep Dive Oracle

Spec / reference chính thức:

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), extends Thread (tránh), ExecutorService (production).
  • start() tạo OS thread mới và chạy run() trên đó; gọi run() 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.
  • InterruptedException throw + clear flag — pattern phải restore bằng Thread.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ước start(). Dùng cho background non-critical.

13. Tự kiểm tra

Tự kiểm tra
Q1
Khác biệt cụ thể giữa thread.run()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.

Q2
Vì 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();

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
worker

Thứ 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()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.

Q4
Vì sao trong code production không nên dùng extends Thread?

3 lý do:

  1. 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 được BaseService, AbstractRepository, hay framework base class khác.
  2. Coupling logic với cơ chế: logic task gắn chặt với lifecycle thread. Muốn chạy task trên ExecutorService pool thay vì thread dedicated — phải refactor. Nếu tách thành Runnable, task là task, cơ chế chạy tách biệt.
  3. Composition over inheritance (module 6): Runnable biể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.

Q5
Vì 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.

Q6
Giả 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?

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?