Chuyển tới nội dung chính

Tạo và quản lý Thread

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Biết 3 cách tạo Thread trong Java (extends Thread, implements Runnable, Callable)
  • Hiểu sự khác biệt giữa Thread và Runnable
  • Sử dụng được các methods quản lý thread: start(), sleep(), join(), interrupt()
  • Nắm được khái niệm daemon thread và thread priority
  • Biết cách đặt tên threads để dễ debug

Bài trước: Giới thiệu Multithreading — Đã tìm hiểu về Process, Thread, Concurrency và Race Conditions. Bài này sẽ hướng dẫn chi tiết cách tạo và quản lý threads trong Java.

3 cách tạo Thread trong Java

Cách 1: Extends Thread class

public class MyThread extends Thread {
private String name;

public MyThread(String name) {
this.name = name;
}

@Override
public void run() {
// Code thực thi trong thread
for (int i = 1; i <= 5; i++) {
System.out.println(name + " - Count: " + i);
try {
Thread.sleep(1000); // Nghỉ 1 giây
} catch (InterruptedException e) {
System.out.println(name + " interrupted");
}
}
}

public static void main(String[] args) {
MyThread t1 = new MyThread("Thread-1");
MyThread t2 = new MyThread("Thread-2");

t1.start(); // Bắt đầu thread 1
t2.start(); // Bắt đầu thread 2

System.out.println("Main thread continues...");
}
}

Output (không theo thứ tự cố định):

Main thread continues...
Thread-1 - Count: 1
Thread-2 - Count: 1
Thread-1 - Count: 2
Thread-2 - Count: 2
...
cảnh báo

Hạn chế: Java chỉ cho phép single inheritance. Nếu class đã extends class khác thì không thể extends Thread.

public class MyRunnable implements Runnable {
private String name;

public MyRunnable(String name) {
this.name = name;
}

@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(name + " - Count: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(name + " interrupted");
}
}
}

public static void main(String[] args) {
MyRunnable r1 = new MyRunnable("Runnable-1");
MyRunnable r2 = new MyRunnable("Runnable-2");

Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);

t1.start();
t2.start();
}
}

Lambda expression (Java 8+):

public class RunnableLambdaDemo {
public static void main(String[] args) {
// Cách ngắn gọn với lambda
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Lambda Thread - " + i);
try { Thread.sleep(1000); }
catch (InterruptedException e) { }
}
});

t1.start();
}
}
Tại sao Runnable tốt hơn?
  1. Flexibility: Class có thể extends class khác và implements Runnable
  2. Separation of concerns: Logic (Runnable) tách biệt khỏi thread management (Thread)
  3. Reusability: Một Runnable có thể được dùng cho nhiều threads
  4. Functional interface: Dùng được lambda expression

Cách 3: Implements Callable<V> với Future

Callable khác Runnable ở chỗ:

  • Có thể return kết quả
  • Có thể throw checked exception
import java.util.concurrent.*;

public class CallableDemo {
public static void main(String[] args) throws Exception {
// Tạo Callable task
Callable<Integer> task = () -> {
System.out.println("Calculating sum...");
Thread.sleep(2000);
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum; // Trả về kết quả
};

// Cần ExecutorService để chạy Callable
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);

System.out.println("Task submitted. Doing other work...");

// Lấy kết quả (blocking call)
Integer result = future.get(); // Chờ cho đến khi task hoàn thành
System.out.println("Sum: " + result);

executor.shutdown();
}
}

Output:

Task submitted. Doing other work...
Calculating sum...
Sum: 5050
Future methods
  • get(): Chờ và lấy kết quả (blocking)
  • get(timeout, unit): Chờ tối đa timeout
  • isDone(): Kiểm tra task đã hoàn thành chưa
  • cancel(mayInterrupt): Hủy task

So sánh Thread vs Runnable vs Callable

Đặc điểmThreadRunnableCallable
Cách khai báoextends Threadimplements Runnableimplements Callable<V>
Methodrun()run()call()
Return valueKhôngKhôngCó (generic type)
ExceptionKhông throwsKhông throwsCó thể throws Exception
Sử dụngthread.start()new Thread(runnable).start()executor.submit(callable)
Kết quảKhông thể lấy trực tiếpKhông thể lấy trực tiếpFuture<V>
InheritanceKhông extends được class khácCó thể extends class khácCó thể extends class khác

Thread Methods quan trọng

1. start() vs run()

Thread t = new Thread(() -> System.out.println("Hello from thread"));

// ĐÚNG: Tạo thread mới
t.start();

// SAI: Chỉ gọi method run() trong main thread, không tạo thread mới!
t.run();
cảnh báo
  • start(): Tạo thread mới và gọi run() trong thread đó
  • run(): Thực thi code trong thread hiện tại (không tạo thread mới)

start() hoạt động như thế nào?

Khi gọi start(), JVM thực hiện các bước sau:

  1. Kiểm tra thread state (nếu đã started → IllegalThreadStateException)
  2. Đăng ký thread với thread group
  3. Gọi native method start0() để yêu cầu OS tạo thread mới
  4. OS scheduler khởi tạo thread và chuyển từ NEWRUNNABLE state
  5. Khi thread được scheduled, JVM gọi method run() trong thread context mới đó

Đó là lý do tại sao gọi run() trực tiếp chỉ thực thi trong current thread — không có bước 3, 4, 5!

2. sleep(milliseconds)

Tạm dừng thread trong khoảng thời gian nhất định:

public class SleepDemo {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Count: " + i);
try {
Thread.sleep(1000); // Ngủ 1 giây
} catch (InterruptedException e) {
System.out.println("Thread interrupted during sleep");
}
}
});

t.start();
}
}
mẹo

Thread.sleep() là static method, nó luôn tạm dừng current thread (thread đang gọi nó), không phải thread object.

Quan trọng: sleep() KHÔNG giải phóng lock nếu thread đang trong synchronized block. Khác với wait() (sẽ học ở bài Synchronization).

3. join()

Chờ thread khác hoàn thành:

public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker: Starting work...");
try {
Thread.sleep(3000); // Giả lập công việc mất 3 giây
} catch (InterruptedException e) { }
System.out.println("Worker: Work completed!");
});

worker.start();
System.out.println("Main: Waiting for worker to finish...");

worker.join(); // Main thread chờ worker thread kết thúc

System.out.println("Main: Worker finished. Continuing...");
}
}

Output:

Main: Waiting for worker to finish...
Worker: Starting work...
Worker: Work completed!
Main: Worker finished. Continuing...

join() với timeout:

worker.join(2000); // Chờ tối đa 2 giây
if (worker.isAlive()) {
System.out.println("Worker still running after 2 seconds");
}

4. interrupt() - Cơ chế hợp tác (cooperative)

Gửi tín hiệu interrupt tới thread:

public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
System.out.println("Working... " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Thread was interrupted!");
// Clean up resources
return;
}
});

t.start();
Thread.sleep(3000); // Chờ 3 giây

System.out.println("Main: Interrupting thread...");
t.interrupt(); // Gửi interrupt signal
}
}

Interrupt là cơ chế hợp tác (cooperative mechanism):

Interrupt KHÔNG force stop thread, nó chỉ đặt một interrupt flag lên thread. Thread phải tự kiểm tra và phản hồi. Có 2 cách phản hồi interrupt:

1. InterruptedException (blocking methods):

  • Các method như sleep(), wait(), join() tự động kiểm tra interrupt flag
  • Nếu flag được set, chúng throw InterruptedExceptionclear flag
  • Thread phải catch exception và quyết định xử lý (return, cleanup, re-interrupt)

2. Thread.interrupted() / isInterrupted() (non-blocking code):

  • Dùng cho code không blocking (vòng lặp, tính toán)
  • Thread.interrupted(): STATIC method, check flag của current threadCLEAR flag
  • thread.isInterrupted(): INSTANCE method, check flag KHÔNG clear
// Cách 1: Thread.interrupted() (clears flag)
public void run() {
while (!Thread.interrupted()) { // Kiểm tra và clear flag
// Do work
processData();
}
System.out.println("Thread stopped gracefully");
}

// Cách 2: isInterrupted() (doesn't clear)
public void run() {
while (!Thread.currentThread().isInterrupted()) {
processData();
if (Thread.currentThread().isInterrupted()) {
cleanup();
break;
}
}
}

// Cách 3: Handle exception và re-interrupt nếu cần propagate
public void run() {
try {
while (true) {
doWork();
Thread.sleep(100);
}
} catch (InterruptedException e) {
// Re-set interrupt flag nếu cần propagate lên caller
Thread.currentThread().interrupt();
cleanup();
}
}
Lưu ý quan trọng
  • InterruptedException clear flag: Sau khi throw, flag = false
  • Thread.interrupted() clear flag: Sau khi gọi, flag = false
  • isInterrupted() KHÔNG clear flag: Flag vẫn còn sau khi check
  • Interrupt không làm thread dừng ngay lập tức — thread phải cooperative

5. isAlive()

Kiểm tra thread còn đang chạy không:

Thread t = new Thread(() -> {
try { Thread.sleep(2000); }
catch (InterruptedException e) { }
});

System.out.println("Before start: " + t.isAlive()); // false
t.start();
System.out.println("After start: " + t.isAlive()); // true
Thread.sleep(3000);
System.out.println("After finish: " + t.isAlive()); // false

Thread Priority

Thread có độ ưu tiên từ 1 (thấp nhất) đến 10 (cao nhất):

public class PriorityDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Low priority: " + i);
}
});

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("High priority: " + i);
}
});

t1.setPriority(Thread.MIN_PRIORITY); // 1
t2.setPriority(Thread.MAX_PRIORITY); // 10

t1.start();
t2.start();
}
}

Constants:

  • Thread.MIN_PRIORITY = 1
  • Thread.NORM_PRIORITY = 5 (default)
  • Thread.MAX_PRIORITY = 10
cảnh báo

Priority chỉ là gợi ý cho thread scheduler. Không đảm bảo high priority thread sẽ chạy trước. Phụ thuộc vào OS và JVM implementation.

Daemon Threads vs User Threads

User Threads (default)

  • JVM chờ tất cả user threads kết thúc mới thoát
  • Main thread là user thread

Daemon Threads (nền/hậu cảnh)

Daemon threads là background threads phục vụ cho user threads. Ví dụ:

  • Garbage Collector: Thu gom rác
  • Finalizer thread: Thực thi finalizers
  • Signal dispatcher: Xử lý OS signals
  • Reference Handler: Quản lý reference objects

Đặc điểm quan trọng:

  • JVM KHÔNG CHỜ daemon threads hoàn thành khi tất cả user threads đã kết thúc
  • Daemon threads bị KILL ĐỘT NGỘT khi JVM shutdown — có thể dừng giữa chừng một operation
public class DaemonDemo {
public static void main(String[] args) {
Thread daemon = new Thread(() -> {
while (true) {
System.out.println("Daemon running...");
try { Thread.sleep(500); }
catch (InterruptedException e) { }
}
});

daemon.setDaemon(true); // Phải set TRƯỚC khi start()
daemon.start();

// Main thread (user thread) ngủ 2 giây rồi kết thúc
try { Thread.sleep(2000); }
catch (InterruptedException e) { }

System.out.println("Main thread ending...");
// JVM thoát, daemon thread bị kill ngay cả khi đang trong vòng lặp
}
}
Quy tắc setDaemon()

setDaemon(true) PHẢI được gọi TRƯỚC start(). Nếu gọi sau → IllegalThreadStateException

Thread t = new Thread(() -> doWork());
t.start();
t.setDaemon(true); // ❌ RUNTIME EXCEPTION!
Không dùng Daemon cho critical tasks

KHÔNG dùng daemon threads cho:

  • File I/O operations (file có thể bị corrupt khi kill giữa chừng)
  • Database transactions (có thể mất dữ liệu)
  • Network operations (connection không đóng đúng cách)
  • Critical cleanup tasks

Daemon threads nên chỉ dùng cho non-critical background tasks như monitoring, logging, cache refresh.

Use cases phù hợp cho Daemon threads
  • Monitoring threads (health checks)
  • Background logging threads
  • Periodic cache refresh
  • Session cleanup schedulers
  • Metrics collection

Virtual Threads (Java 21+)

Java 21+ Feature

Virtual threads là một trong những tính năng quan trọng nhất của Project Loom (Java 21+).

Vấn đề với Platform Threads truyền thống:

  • Mỗi platform thread (traditional Java thread) map 1:1 với OS thread
  • OS threads rất nặng (≈2MB stack mỗi thread)
  • Giới hạn thực tế: ~10,000 platform threads trên một máy
  • Blocking I/O lãng phí CPU vì thread phải chờ

Virtual Threads giải quyết:

  • Lightweight: ~1KB stack per virtual thread
  • Managed by JVM: JVM scheduler, không phải OS
  • Có thể tạo hàng triệu virtual threads (tested với 1 million+)
  • Không cần thread pooling — tạo thread per request là OK!
// Platform thread (traditional)
Thread platformThread = Thread.ofPlatform()
.name("platform-worker")
.start(() -> doWork());

// Virtual thread (Java 21+)
Thread virtualThread = Thread.ofVirtual()
.name("virtual-worker")
.start(() -> doWork());

// ExecutorService với virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> handleRequest());
}
} // Auto shutdown khi close

// Hoặc tạo đơn giản
Thread.startVirtualThread(() -> processData());

Sự khác biệt chính:

Platform ThreadVirtual Thread
Weight~2MB~1KB
Managed byOSJVM
Limit~10KMillions
PoolingCần pool (expensive)Không cần (cheap)
Best forCPU-bound tasksI/O-bound tasks

Giới hạn của Virtual Threads:

cảnh báo
  • KHÔNG phù hợp với CPU-bound tasks: Virtual threads vẫn chạy trên platform threads (carrier threads)
  • Tránh synchronized: Dùng ReentrantLock thay thế (synchronized pin carrier thread)
  • ThreadLocal có cost: Mỗi virtual thread có riêng ThreadLocal → memory overhead nếu dùng nhiều
// ❌ Tránh synchronized với virtual threads
synchronized(lock) {
// Pin carrier thread!
}

// ✅ Dùng ReentrantLock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// Không pin carrier thread
} finally {
lock.unlock();
}

Virtual threads là game-changer cho high-throughput I/O applications (web servers, microservices)!

Thread Naming

public class ThreadNamingDemo {
public static void main(String[] args) {
// Cách 1: Đặt tên qua constructor
Thread t1 = new Thread(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
}, "Worker-1");

// Cách 2: Dùng setName()
Thread t2 = new Thread(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
});
t2.setName("Worker-2");

t1.start();
t2.start();

// Current thread
System.out.println("Main thread: " + Thread.currentThread().getName());
}
}

Output:

Main thread: main
Running in: Worker-1
Running in: Worker-2

Ví dụ thực hành: Download Manager

import java.util.ArrayList;
import java.util.List;

class FileDownloader implements Runnable {
private String fileName;
private int sizeInMB;

public FileDownloader(String fileName, int sizeInMB) {
this.fileName = fileName;
this.sizeInMB = sizeInMB;
}

@Override
public void run() {
System.out.println("Downloading " + fileName + " (" + sizeInMB + " MB)...");

for (int i = 10; i <= 100; i += 10) {
try {
Thread.sleep(500); // Giả lập thời gian download
} catch (InterruptedException e) {
System.out.println(fileName + " download interrupted");
return;
}
System.out.println(fileName + ": " + i + "% complete");
}

System.out.println(fileName + " downloaded successfully!");
}
}

public class DownloadManager {
public static void main(String[] args) throws InterruptedException {
List<Thread> downloads = new ArrayList<>();

// Tạo 3 download tasks
String[] files = {"video.mp4", "music.mp3", "document.pdf"};
int[] sizes = {100, 50, 20};

for (int i = 0; i < files.length; i++) {
Thread t = new Thread(
new FileDownloader(files[i], sizes[i]),
"Downloader-" + (i + 1)
);
downloads.add(t);
t.start();
}

// Chờ tất cả downloads hoàn thành
for (Thread t : downloads) {
t.join();
}

System.out.println("\n✓ All downloads completed!");
}
}

Best Practices

mẹo
  1. Prefer Runnable over Thread: Linh hoạt hơn, dùng được lambda
  2. Always handle InterruptedException: Không bỏ qua exception
  3. Name your threads: Dễ debug
  4. Use join() khi cần chờ: Đừng dùng while(thread.isAlive())
  5. Don't call run() directly: Luôn dùng start()
  6. Set daemon before start(): setDaemon() phải gọi trước start()

Bài tập

Bài 1: Thread Basics

Tạo 3 threads:

  • Thread 1: In số lẻ từ 1-99
  • Thread 2: In số chẵn từ 2-100
  • Thread 3: In số chia hết cho 5 từ 5-100

Chạy đồng thời và quan sát output.

Bài 2: Callable và Future

Viết 3 Callable tasks:

  1. Tính tổng 1 đến 1000
  2. Tính tổng bình phương 1 đến 100
  3. Tính giai thừa của 10

Chạy song song và in kết quả.

Bài 3: Download Manager nâng cao

Mở rộng DownloadManager với:

  • Progress bar cho mỗi download
  • Khả năng pause/resume (dùng interrupt)
  • Giới hạn tối đa 2 downloads đồng thời
  • Tính tổng thời gian download
OCP Exam Tips

Những điểm quan trọng cho kỳ thi:

  1. start() called twice → IllegalThreadStateException

    Thread t = new Thread(() -> {});
    t.start();
    t.start(); // ❌ IllegalThreadStateException
  2. sleep() KHÔNG giải phóng lock (synchronized)

    • Thread.sleep() vẫn giữ lock nếu đang trong synchronized block
    • Khác với wait() (sẽ học ở bài Synchronization)
  3. run() vs start()

    • run(): Thực thi trong CALLER thread (không tạo thread mới)
    • start(): Tạo NEW thread và gọi run() trong đó
  4. Thread.yield() chỉ là hint

    • Không đảm bảo thread khác sẽ chạy
    • Scheduler có thể ignore
  5. Runnable vs Callable

    • Runnable: void run(), không throws exception
    • Callable: V call() throws Exception, trả về kết quả qua Future
  6. setDaemon() must be before start()

    thread.start();
    thread.setDaemon(true); // ❌ IllegalThreadStateException
  7. Thread.interrupted() vs isInterrupted()

    • Thread.interrupted(): STATIC, check current thread, CLEAR flag
    • thread.isInterrupted(): INSTANCE, KHÔNG clear flag
  8. Thread priority không đảm bảo thứ tự thực thi

    • Chỉ là hint cho scheduler
    • Phụ thuộc OS và JVM implementation
  9. InterruptedException clears interrupt flag

    • Sau khi throw, flag = false
    • Nếu cần propagate, phải re-interrupt: Thread.currentThread().interrupt()

Tóm tắt

  • 3 cách tạo thread: extends Thread, implements Runnable (recommended), implements Callable
  • start() vs run(): start() gọi native start0() để tạo OS thread, run() chỉ gọi method
  • Thread methods: sleep(), join(), interrupt(), isAlive()
  • Interrupt: Cơ chế hợp tác (cooperative), set flag, thread tự check và phản hồi
  • Priority: 1-10, chỉ là gợi ý cho scheduler (preemptive - ưu tiên/chiếm quyền)
  • Daemon threads: Background threads, JVM không chờ, bị kill đột ngột khi shutdown
  • Virtual Threads (Java 21+): Lightweight, millions possible, dùng cho I/O-bound tasks
  • Thread naming: Giúp debug dễ dàng

Bài tiếp theo sẽ học về Synchronization - cách giải quyết race conditions!

Đọc thêm

Official Documentation:

Books:

  • Effective Java (3rd Edition) by Joshua Bloch
    • Item 81: Prefer concurrency utilities to wait and notify
    • Item 80: Prefer executors to threads
  • Java Concurrency in Practice by Brian Goetz (Chapter 5: Building Blocks)

JLS (Java Language Specification):

Related lessons: