CodeGym /Các khóa học /JAVA 25 SELF /Trạng thái và vòng đời của luồng

Trạng thái và vòng đời của luồng

JAVA 25 SELF
Mức độ, Bài học
Có sẵn

1. Các trạng thái chính của luồng trong Java

Trong Java, một luồng không chỉ đơn giản là “đang chạy” hay “đã dừng”. Nó có một vòng đời đầy đủ, và ở mỗi giai đoạn luồng sẽ cư xử khác nhau. Hiểu các giai đoạn này là chìa khóa để viết ứng dụng đa luồng ổn định và để gỡ lỗi các hiện tượng “treo” và “kết thúc bất ngờ”.

Có những trạng thái nào?

Java định nghĩa các trạng thái chính của luồng (chúng được liệt kê trong enum Thread.State):

Trạng thái Mô tả
NEW
Luồng đã được tạo nhưng chưa được khởi chạy (new Thread(...), nhưng start() chưa được gọi)
RUNNABLE
Luồng sẵn sàng thực thi hoặc đang thực thi ngay lúc này
BLOCKED
Luồng đang chờ monitor được giải phóng (bị chặn tại khối synchronized)
WAITING
Luồng đang chờ được luồng khác “đánh thức” (ví dụ qua Object.wait())
TIMED_WAITING
Luồng chờ với thời hạn (ví dụ Thread.sleep(1000), wait(1000), join(1000))
TERMINATED
Luồng đã kết thúc thực thi

Thông tin thú vị:
Trong các sách và bài viết cũ, bạn có thể bắt gặp các tên gọi khác hoặc sơ đồ hơi khác. Nhưng từ Java 5+ các trạng thái này được coi là tiêu chuẩn.

Sơ đồ trực quan vòng đời của luồng

stateDiagram-v2
    [*] --> NEW
    NEW --> RUNNABLE: start()
    RUNNABLE --> BLOCKED: cố gắng vào synchronized, nhưng monitor đang bận
    BLOCKED --> RUNNABLE: monitor đã được giải phóng
    RUNNABLE --> WAITING: wait(), join()
    RUNNABLE --> TIMED_WAITING: sleep(), wait(timeout), join(timeout)
    WAITING --> RUNNABLE: notify()/notifyAll(), join() đã kết thúc
    TIMED_WAITING --> RUNNABLE: hết thời hạn / notify()/notifyAll()
    RUNNABLE --> TERMINATED: run() đã kết thúc
    WAITING --> TERMINATED: run() đã kết thúc (hiếm)
    TIMED_WAITING --> TERMINATED: run() đã kết thúc (hiếm)

2. Các phương thức điều khiển luồng

Ngủ: Thread.sleep(long millis)

Đôi khi luồng cần “ngủ” để không cản trở các luồng khác hoặc chờ sự kiện. Phương thức Thread.sleep(ms) đưa luồng vào trạng thái

TIMED_WAITING
trong số mili-giây đã chỉ định.

System.out.println("Luồng ngủ trong 2 giây...");
Thread.sleep(2000); // Ngủ 2 giây
System.out.println("Luồng đã thức dậy!");
  • Sau khi ngủ xong, luồng quay trở lại trạng thái
    RUNNABLE
    (sẵn sàng làm việc).
  • Nếu luồng bị ngắt trong khi ngủ, sẽ ném InterruptedException.

Chờ một luồng khác kết thúc: join()

Đôi khi bạn không chỉ khởi chạy luồng, mà còn cần chờ nó hoàn thành. Trong Java có phương thức join():

Thread t = new Thread(() -> {
    System.out.println("Đang làm việc...");
    try { Thread.sleep(1000); } catch (InterruptedException e) {}
    System.out.println("Xong!");
});
t.start();
System.out.println("Đang chờ luồng t kết thúc...");
t.join(); // Luồng hiện tại (ví dụ main) chờ t
System.out.println("Luồng t đã kết thúc!");

Ở đây luồng chính bắt đầu chờ ngay khi gọi join(). Trong suốt thời gian này, nó ở trạng thái

WAITING
cho đến khi luồng t hoàn thành. Cũng có biến thể với thời hạn — join(long millis). Khi đó luồng chờ một khoảng thời gian giới hạn và trạng thái của nó sẽ là
TIMED_WAITING
.

Ngắt luồng: interrupt()

Có tình huống bạn cần lịch sự yêu cầu luồng kết thúc, ví dụ khi người dùng nhấn nút “Hủy”. Khi đó dùng phương thức interrupt():

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Đang làm việc...
    }
    System.out.println("Luồng kết thúc do bị ngắt!");
});
t.start();
// ... sau một thời gian:
t.interrupt(); // Tín hiệu cho luồng: "đến lúc dừng lại"

Điều quan trọng cần hiểu là gọi interrupt() không giết luồng ngay lập tức. Nó chỉ đặt một cờ đặc biệt. Chính luồng đó cần định kỳ kiểm tra cờ này qua isInterrupted() và tự kết thúc. Nếu lúc đó luồng đang ở trạng thái ngủ (sleep()) hoặc chờ (wait()), nó không chỉ thấy cờ mà sẽ nhận ngay ngoại lệ InterruptedException. Đó chính là cách “đúng chuẩn” để dừng luồng trong Java: chương trình không cưỡng bức ngắt luồng, mà thông báo rằng đã đến lúc kết thúc.

3. Ví dụ về các chuyển trạng thái

Hãy xem qua các ví dụ về cách một luồng “di chuyển” qua các trạng thái của mình.

Ví dụ 1:
NEW
→
RUNNABLE
→
TERMINATED

Thread t = new Thread(() -> System.out.println("Xin chào!"));
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (hoặc TERMINATED, nếu luồng rất nhanh)
t.join();
System.out.println(t.getState()); // TERMINATED

Ví dụ 2:
RUNNABLE
→
TIMED_WAITING
→
RUNNABLE
→
TERMINATED

Thread t = new Thread(() -> {
    try {
        System.out.println("Đang ngủ...");
        Thread.sleep(1000); // TIMED_WAITING
        System.out.println("Đã thức dậy!");
    } catch (InterruptedException e) {
        System.out.println("Luồng đã bị ngắt!");
    }
});
t.start();

Ví dụ 3:
RUNNABLE
→
WAITING
với join()

Thread t1 = new Thread(() -> {
    try { Thread.sleep(500); } catch (InterruptedException ignored) {}
    System.out.println("t1 đã kết thúc");
});
Thread t2 = new Thread(() -> {
    try {
        t1.join(); // t2 chờ t1, ở trạng thái WAITING
        System.out.println("t2 đã đợi xong t1");
    } catch (InterruptedException ignored) {}
});
t1.start();
t2.start();

Ví dụ 4:
BLOCKED

Object lock = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lock) {
        try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
        System.out.println("t1 đã thoát khỏi khối synchronized");
    }
});
Thread t2 = new Thread(() -> {
    synchronized (lock) {
        System.out.println("t2 đã vào synchronized");
    }
});
t1.start();
Thread.sleep(100); // Cho t1 chiếm lock
t2.start();
Thread.sleep(100); // Cho t2 thử vào synchronized
System.out.println("Trạng thái của t2: " + t2.getState()); // BLOCKED

4. Làm sao biết trạng thái của luồng? Các phương thức isAlive() và getState()

  • isAlive() — trả về true nếu luồng đã được khởi chạy và chưa kết thúc (
    TERMINATED
    — khi đó là false).
  • getState() — trả về trạng thái hiện tại của luồng (một giá trị của enum Thread.State).
Thread t = new Thread(() -> {});
System.out.println(t.isAlive()); // false (NEW)
t.start();
System.out.println(t.isAlive()); // true (RUNNABLE/WAITING/...)
t.join();
System.out.println(t.isAlive()); // false (TERMINATED)

5. Vì sao không thể “giết” một luồng trực tiếp và các lời khuyên thực tiễn khác

Không có phương thức “kill”!

Trong Java không có phương thức cho phép “giết” luồng theo lệnh. Tại sao? Vì điều đó không an toàn: nếu luồng đang giữ tài nguyên nào đó (tệp, kết nối, khóa), việc ép hủy có thể khiến hệ thống rơi vào trạng thái không nhất quán.

Các phương thức lỗi thời: stop(), suspend(), resume()

Trong các phiên bản Java rất cũ có các phương thức stop(), suspend(), resume(). Giờ đây chúng được đánh dấu @Deprecated và tuyệt đối không khuyến nghị sử dụng. Vì sao?

  • stop() có thể giết luồng ở bất kỳ thời điểm nào, để lại dữ liệu ở trạng thái không nhất quán.
  • suspend() có thể “đóng băng” một luồng đang giữ khóa, khiến cả chương trình treo.
  • resume() đôi khi không thể “rã băng” luồng nếu nó đã kết thúc.

Cách tiếp cận hiện đại:
Luồng nên tự kết thúc một cách chuẩn mực bằng cách phản hồi cờ ngắt (isInterrupted()) hoặc các tín hiệu khác.

Thực hành tốt

  • Không gọi các phương thức đã bị coi là lỗi thời.
  • Sử dụng cờ ngắt để dừng luồng (interrupt() và kiểm tra isInterrupted()).
  • Theo dõi các trạng thái luồng khi gỡ lỗi — điều này giúp phát hiện treo và deadlock.
  • Đừng quên join() nếu cần chờ luồng kết thúc công việc.

6. Lỗi thường gặp khi làm việc với vòng đời luồng

Lỗi số 1: Khởi chạy lại cùng một luồng.
Trong Java, một luồng chỉ có thể được khởi chạy một lần. Nếu gọi start() lần thứ hai — bạn sẽ nhận IllegalThreadStateException. Nếu cần lặp lại tác vụ — hãy tạo đối tượng Thread mới.

Thread t = new Thread(() -> {});
t.start();
t.start(); // Sẽ ném ngoại lệ!

Lỗi số 2: Nhầm lẫn giữa run() và start().
Gọi run() trực tiếp không chạy mã trong luồng mới — nó chạy trong luồng hiện tại (ví dụ main). Chỉ start() mới thực sự khởi chạy một luồng mới.

Lỗi số 3: Không xử lý InterruptedException.
Nếu luồng đang ngủ hoặc chờ và bị ngắt, InterruptedException sẽ được ném ra. Nếu bỏ qua, luồng có thể “ngủ mãi mãi” hoặc kết thúc bất ngờ.

Lỗi số 4: Không kiểm tra trạng thái của luồng.
Đôi khi chương trình bị treo vì một luồng đang chờ luồng khác vốn đã kết thúc hoặc sẽ chẳng bao giờ bắt đầu. Hãy dùng getState() và isAlive() để chẩn đoán.

Lỗi số 5: Dùng các phương thức điều khiển luồng lỗi thời.
Các phương thức stop(), suspend(), resume() là tệ. Đừng dùng chúng, kể cả khi rất muốn “sửa nhanh mọi thứ”.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Kiểm tra hoàn thành sứ mệnh không gian 🛰️
Kiểm tra hoàn thành sứ mệnh không gian 🛰️
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Đồng bộ công việc của hai nhân viên 🤝
Đồng bộ công việc của hai nhân viên 🤝
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION