
Khi học Java Multithreading, hầu như ai cũng biết Thread có 6 trạng thái:
- NEW
- RUNNABLE
- BLOCKED
- WAITING
- TIMED_WAITING
- TERMINATED
Tuy nhiên, trong thực tế làm việc, việc chỉ nhớ tên các trạng thái gần như không giúp ích nhiều.
Điều quan trọng hơn là:
- Khi nào thread chuyển sang từng trạng thái?
- JVM làm gì trong mỗi trạng thái?
- Hệ điều hành tham gia như thế nào?
- Làm sao đọc thread dump để tìm bottleneck trên production?
Trong bài viết này, chúng ta sẽ đi từ kiến thức nền tảng đến các tình huống thực tế mà một Senior Java Engineer thường gặp.
Thread là gì?
Thread là đơn vị thực thi nhỏ nhất trong một process.
Một JVM process có thể chứa hàng trăm hoặc hàng nghìn thread chạy đồng thời.
Mỗi thread có tài nguyên riêng:
- Program Counter (PC Register)
- Java Stack
- Native Stack
- Thread State
Trong khi đó các thread cùng chia sẻ:
- Heap
- Method Area
- Runtime Constant Pool
Chính việc chia sẻ Heap khiến lập trình đa luồng trở nên phức tạp vì nhiều thread có thể truy cập cùng một object.
Java Thread Lifecycle
Java định nghĩa vòng đời của thread thông qua enum Thread.State.
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED
}
Lưu ý rằng Java không có trạng thái RUNNING.
Đây là điểm khiến nhiều lập trình viên nhầm lẫn.
Trong JVM, thread đang chạy trên CPU và thread đang chờ CPU đều được xem là RUNNABLE.
Việc thread nào thực sự được CPU thực thi hoàn toàn do Operating System Scheduler quyết định.
NEW
Đây là trạng thái đơn giản nhất.
Thread thread = new Thread(() -> {
System.out.println("Hello");
});
Lúc này:
thread.getState();
Kết quả:
NEW
Thread mới chỉ tồn tại dưới dạng object.
JVM chưa tạo native thread.
Chưa có stack thực thi.
Thread chưa được đăng ký với Operating System Scheduler.
Điều gì xảy ra khi gọi start()
Nhiều người nghĩ:
thread.start();
nghĩa là thread sẽ chạy ngay.
Điều này không chính xác.
Sau khi gọi start():
- JVM tạo native thread.
- Cấp phát stack.
- Đăng ký với OS Scheduler.
- Thread chuyển sang
RUNNABLE.
Việc có được CPU hay không phụ thuộc hoàn toàn vào Scheduler của hệ điều hành.
Do đó hai thread được start() liên tiếp hoàn toàn có thể chạy theo bất kỳ thứ tự nào.
RUNNABLE
Đây là trạng thái dễ gây nhầm nhất.
Trong Java:
RUNNABLE bao gồm cả:
- đang chờ CPU
- đang chạy trên CPU
Ví dụ:
while (true) {
}
Thread luôn ở trạng thái:
RUNNABLE
Ngay cả khi CPU đang thực sự thực thi vòng lặp đó.
Java không cung cấp trạng thái RUNNING riêng biệt.
CPU Scheduler và Context Switching
Giả sử máy chỉ có 2 CPU Core nhưng có 100 thread.
Scheduler sẽ liên tục:
- chọn thread
- lưu register
- lưu Program Counter
- chuyển sang thread khác
Quá trình này gọi là Context Switching.
Context switching quá nhiều sẽ làm giảm hiệu năng do CPU phải liên tục lưu và khôi phục trạng thái của thread thay vì thực thi business logic.
Đây là một trong những lý do Virtual Thread xuất hiện trong Java 21 để giảm chi phí quản lý thread ở tầng ứng dụng, dù việc lập lịch vẫn có sự phối hợp với hệ điều hành.
BLOCKED
BLOCKED xảy ra khi một thread muốn lấy monitor lock nhưng lock đang bị thread khác giữ.
Trạng thái này chỉ xuất hiện với synchronized.
Ví dụ:
public class BlockedDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("Thread-1 acquired lock");
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
System.out.println("Thread-1 released lock");
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("Thread-2 acquired lock");
}
});
t1.start();
Thread.sleep(500);
t2.start();
Thread.sleep(500);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
Output:
Thread-1 acquired lock
TIMED_WAITING
BLOCKED
Thread-1 released lock
Thread-2 acquired lock
Điều đáng chú ý là:
Mặc dù Thread-1 đang sleep(), nó vẫn giữ monitor lock.
Do đó Thread-2 không thể đi vào synchronized và phải chuyển sang BLOCKED.
Đây là lỗi hiểu nhầm rất phổ biến khi mới học Java Concurrency.
WAITING
WAITING xảy ra khi thread chủ động tạm dừng và chờ một sự kiện từ thread khác.
Ví dụ:
thread.join();
hoặc
object.wait();
hoặc
LockSupport.park();
Khác với BLOCKED, thread trong WAITING không chờ để lấy lock mà đang chờ một tín hiệu như:
notify()notifyAll()unpark()- thread được
join()kết thúc
TIMED_WAITING
Đây là phiên bản có timeout của WAITING.
Ví dụ:
Thread.sleep(5000);
hoặc
thread.join(5000);
hoặc
object.wait(3000);
Sau khi timeout kết thúc, thread sẽ quay lại RUNNABLE.
TERMINATED
Khi phương thức run() kết thúc hoặc exception không được bắt, thread sẽ chuyển sang TERMINATED.
Ví dụ:
Thread thread = new Thread(() -> {
System.out.println("Finished");
});
thread.start();
thread.join();
System.out.println(thread.getState());
Output:
TERMINATED
Một thread đã kết thúc không thể được start() lần thứ hai.
Nếu gọi lại:
thread.start();
JVM sẽ ném:
IllegalThreadStateException
BLOCKED và WAITING khác nhau như thế nào?
Đây là câu hỏi rất phổ biến trong các buổi phỏng vấn Senior.
| BLOCKED | WAITING |
|---|---|
| Chờ monitor lock | Chờ một tín hiệu từ thread khác |
| Chỉ xảy ra với synchronized | wait(), join(), park() |
| Không thể tự tiếp tục | Được đánh thức bởi notify(), unpark(), hoặc khi thread được join kết thúc |
Có thể ghi nhớ đơn giản:
BLOCKED = chờ lock
WAITING = chờ event
ReentrantLock có tạo ra BLOCKED không?
Không.
Đây là điểm rất nhiều lập trình viên nhầm.
Ví dụ:
lock.lock();
Nếu lock chưa lấy được, thread không chuyển sang BLOCKED.
Thay vào đó, thread sẽ được quản lý bởi AbstractQueuedSynchronizer (AQS) và thường ở trạng thái WAITING hoặc TIMED_WAITING tùy cách sử dụng (lockInterruptibly(), tryLock(timeout)…).
Nói cách khác:
BLOCKEDgần như gắn với monitor lock (synchronized).- Các lock trong
java.util.concurrent.lockssử dụng cơ chế khác.
Đọc Thread Dump trên Production
Khi ứng dụng có dấu hiệu treo hoặc phản hồi chậm, một trong những bước đầu tiên là lấy thread dump:
jstack <pid>
Ví dụ:
"Thread-18"
java.lang.Thread.State: BLOCKED (on object monitor)
at com.demo.PaymentService.transfer(PaymentService.java:82)
- waiting to lock <0x0000000712345678>
- locked <0x0000000711111111>
Từ thread dump, bạn có thể biết:
- Thread đang ở trạng thái nào.
- Thread đang chờ monitor nào.
- Thread nào đang giữ monitor đó.
- Dòng code cụ thể gây lock contention.
Đây là kỹ năng rất quan trọng khi phân tích các vấn đề về deadlock, lock contention hoặc latency trong hệ thống có TPS cao.
Những hiểu lầm phổ biến
1. RUNNABLE nghĩa là đang chạy
Sai.
RUNNABLE bao gồm cả thread đang chạy và thread đang sẵn sàng chạy.
2. sleep() sẽ giải phóng lock
Sai.
Thread.sleep() chỉ đưa thread vào TIMED_WAITING.
Nếu thread đang ở trong synchronized, monitor lock vẫn được giữ.
3. wait() và sleep() giống nhau
Không.
wait() giải phóng monitor lock và chờ được đánh thức.
sleep() không giải phóng monitor lock, chỉ tạm dừng thực thi trong một khoảng thời gian.
4. interrupt() sẽ dừng thread ngay lập tức
Không.
interrupt() chỉ gửi tín hiệu ngắt. Thread có dừng hay không phụ thuộc vào việc nó có kiểm tra interrupt flag hoặc đang ở một thao tác blocking hỗ trợ interrupt hay không.
5. start() có thể gọi nhiều lần
Sai.
Một instance của Thread chỉ có thể được khởi động một lần. Muốn chạy lại, bạn phải tạo một thread mới.
Kết luận
Thread Lifecycle là nền tảng của lập trình đồng thời trong Java, nhưng giá trị thực sự không nằm ở việc thuộc lòng sáu trạng thái.
Điều quan trọng là hiểu vì sao thread chuyển trạng thái, JVM và hệ điều hành phối hợp như thế nào, và cách những trạng thái đó xuất hiện trong thread dump khi hệ thống gặp sự cố.
Khi bạn có thể nhìn vào một jstack và nhanh chóng nhận ra thread đang BLOCKED vì lock contention, hay đang WAITINGdo join() hoặc park(), bạn không chỉ nắm được lý thuyết mà còn có khả năng chẩn đoán và tối ưu các hệ thống Java chạy ở quy mô lớn. Đây cũng là kỹ năng thường được kỳ vọng ở các Senior Java Engineer trong môi trường production.
Reference
Be First to Comment