1. Vòng đời và các trạng thái của luồng trong Java
Trong môi trường Java, một luồng (thread) trải qua nhiều giai đoạn khác nhau từ khi được tạo ra cho đến khi kết thúc. Hiểu rõ các trạng thái này là nền tảng để làm việc hiệu quả với đa luồng. Các trạng thái cơ bản bao gồm:
-
Mới (New): Luồng ở trạng thái này ngay sau khi đối tượng luồng được khởi tạo nhưng phương thức
start()chưa được gọi. Ví dụ:Thread myThread = new MyCustomThread();. -
Sẵn sàng (Runnable): Khi phương thức
start()của luồng được gọi, luồng chuyển sang trạng thái Sẵn sàng. Điều này có nghĩa là luồng đã sẵn sàng để được bộ lập lịch CPU chọn và cấp phát thời gian xử lý, nhưng nó chưa thực sự chạy. -
Đang chạy (Running): Luồng chuyển từ trạng thái Sẵn sàng sang Đang chạy khi bộ lập lịch CPU cấp phát thời gian xử lý cho nó. Đây là lúc mã trong phương thức
run()của luồng được thực thi. Trạng thái Sẵn sàng là cổng vào duy nhất để một luồng có thể chuyển sang trạng thái Đang chạy. -
Bị chặn (Blocked): Một luồng Đang chạy có thể tạm thời ngừng thực thi và chuyển sang trạng thái Bị chặn vì một số lý do. Luồng ở trạng thái này không sử dụng CPU cho đến khi điều kiện chặn được giải quyết. Có ba loại chặn chính:
-
Chờ đợi (Waiting Blocked): Luồng vào trạng thái này khi gọi phương thức
wait(). Nó sẽ ở trạng thái chờ cho đến khi một luồng khác gọinotify()hoặcnotifyAll(). -
Đồng bộ hóa (Synchronization Blocked): Xảy ra khi một luồng cố gắng giành quyền sở hữu một khóa đồng bộ hóa (ví dụ: với từ khóa
synchronized) nhưng khóa đó đang bị một luồng khác giữ. Luồng sẽ chờ cho đến khi khóa được giải phóng. -
Tạm dừng khác (Other Blocked): Các luồng có thể bị chặn do gọi
sleep()(tạm dừng trong một khoảng thời gian),join()(chờ luồng khác hoàn thành) hoặc thực hiện các thao tác I/O. Khi thời gian chờ kết thúc, luồng được chờ hoàn thành, hoặc thao tác I/O hoàn tất, luồng sẽ quay lại trạng thái Sẵn sàng.
-
Chờ đợi (Waiting Blocked): Luồng vào trạng thái này khi gọi phương thức
-
Kết thúc (Terminated/Dead): Luồng vào trạng thái này khi phương thức
run()của nó hoàn thành việc thực thi, hoặc khi một ngoại lệ không được xử lý làm luồng kết thúc. Sau khi một luồng đã kết thúc, nó không thể được khởi động lại.
2. Các phương pháp tạo và khởi động luồng trong Java
Trong Java, có ba cách phổ biến để tạo và quản lý các luồng mới:
2.1. Kế thừa lớp Thread và ghi đè phương thức run()
Cách tiếp cận này yêu cầu bạn định nghĩa một lớp mới kế thừa từ java.lang.Thread và cung cấp cài đặt cho phương thức run(). Phương thức run() chứa logic mà luồng sẽ thực thi.
class MyTaskThread extends Thread {
private int loopCounter = 0;
@Override
public void run() {
for (loopCounter = 0; loopCounter < 50; loopCounter++) {
System.out.println(Thread.currentThread().getName() + " - Iteration: " + loopCounter);
}
}
public static void main(String[] args) {
for (int i = 0; i < 70; i++) {
System.out.println("Main Thread - Progress: " + i);
if (i == 25) {
MyTaskThread firstWorker = new MyTaskThread(); // Luồng mới được tạo (trạng thái New)
MyTaskThread secondWorker = new MyTaskThread(); // Luồng mới khác
firstWorker.start(); // Gọi start() đưa luồng vào trạng thái Sẵn sàng
secondWorker.start(); // Khởi động luồng thứ hai
}
}
}
}
Khi một đối tượng của MyTaskThread được tạo, một luồng mới được hình thành ở trạng thái Mới. Việc gọi start() sẽ đưa nó vào trạng thái Sẵn sàng, chờ đợi bộ lập lịch CPU phân bổ thời gian chạy.
2.2. Triển khai giao diện Runnable và ghi đè phương thức run()
Đây là phương pháp được khuyến nghị hơn, đặc biệt khi lớp của bạn cần kế thừa từ một lớp khác. Bạn tạo một lớp triển khai giao diện java.lang.Runnable và ghi đè phương thức run(). Sau đó, một thể hiện của lớp này được truyền vào constructor của Thread để tạo luồng.
class MyWorker implements Runnable {
private int currentProgress = 0;
@Override
public void run() {
for (currentProgress = 0; currentProgress < 60; currentProgress++) {
System.out.println(Thread.currentThread().getName() + " >> Task progress: " + currentProgress);
}
}
public static void main(String[] args) {
for (int mainLoopCount = 0; mainLoopCount < 70; mainLoopCount++) {
System.out.println("Main flow - Count: " + mainLoopCount);
if (mainLoopCount == 20) {
MyWorker workerTask = new MyWorker(); // Đối tượng Runnable
Thread threadAlpha = new Thread(workerTask, "Worker-Alpha"); // Tạo luồng từ Runnable
Thread threadBeta = new Thread(workerTask, "Worker-Beta"); // Chia sẻ cùng một đối tượng Runnable
threadAlpha.start(); // Khởi động luồng Alpha
threadBeta.start(); // Khởi động luồng Beta
}
}
}
}
Trong ví dụ trên, hai luồng threadAlpha và threadBeta chia sẻ cùng một đối tượng MyWorker. Điều này minh họa cách các luồng có thể chia sẻ dữ liệu khi sử dụng giao diện Runnable.
Mối quan hệ giữa Thread và Runnable
Cả hai phương pháp đều dẫn đến việc thực thi mã trong phương thức run(). Lớp Thread thực tế cũng triển khai giao diện Runnable. Khi bạn tạo một Thread và truyền một Runnable vào, phương thức run() của lớp Thread sẽ kiểm tra xem có đối tượng Runnable nào được cung cấp không. Nếu có, nó sẽ gọi phương thức run() của đối tượng Runnable đó. Nếu bạn ghi đè run() trong một lớp con của Thread và truyền một Runnable vào constructor, thì phương thức run() của lớp con Thread sẽ được ưu tiên thực thi.
class MyWorkerRunnable implements Runnable {
private int dataValue = 0;
@Override
public void run() {
System.out.println("--- Inside MyWorkerRunnable's run() method ---");
for (dataValue = 0; dataValue < 20; dataValue++) {
System.out.println(Thread.currentThread().getName() + " processing " + dataValue);
}
}
}
class CustomThread extends Thread {
private int internalCount = 0;
public CustomThread(Runnable target) {
super(target); // Gọi constructor của lớp cha với Runnable
}
@Override
public void run() {
// Nếu không gọi super.run() hoặc không có target, thì sẽ chạy code này
System.out.println("--- Inside CustomThread's run() method ---");
for (internalCount = 0; internalCount < 20; internalCount++) {
System.out.println(Thread.currentThread().getName() + " calculating " + internalCount);
}
}
public static void main(String[] args) {
for (int i = 0; i < 30; i++) {
System.out.println("Main thread progress: " + i);
if (i == 10) {
Runnable myWorker = new MyWorkerRunnable();
Thread workerThread = new CustomThread(myWorker); // Truyền MyWorkerRunnable vào CustomThread
workerThread.start();
}
}
}
}
Trong ví dụ trên, mặc dù CustomThread được tạo với một đối tượng MyWorkerRunnable, nhưng phương thức run() của CustomThread mới là cái được thực thi. Điều này xảy ra do tính đa hình: phương thức run() của lớp con sẽ được gọi trước.
2.3. Sử dụng Callable và Future
Khi bạn cần một luồng trả về kết quả sau khi hoàn thành nhiệm vụ hoặc ném ra một ngoại lệ, Callable và Future là lựa chọn phù hợp. Callable tương tự như Runnable nhưng phương thức call() của nó có thể trả về một giá trị và khai báo ngoại lệ.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class CalculationTask implements Callable<Integer> {
private int currentVal = 0;
@Override
public Integer call() throws Exception {
int totalSum = 0;
System.out.println(Thread.currentThread().getName() + " - Starting calculation...");
for (; currentVal < 30; currentVal++) {
System.out.println(Thread.currentThread().getName() + " Adding: " + currentVal);
totalSum += currentVal;
Thread.sleep(10); // Mô phỏng công việc
}
System.out.println(Thread.currentThread().getName() + " - Calculation finished.");
return totalSum;
}
}
public class CallableDemo {
public static void main(String[] args) {
Callable<Integer> myCalculation = new CalculationTask();
// FutureTask bao bọc Callable và triển khai cả Runnable và Future
FutureTask<Integer> taskFuture = new FutureTask<>(myCalculation);
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " - Main loop iteration " + i);
if (i == 10) {
Thread computationThread = new Thread(taskFuture, "ComputeThread");
computationThread.start(); // Khởi động luồng
}
}
System.out.println("Main thread has finished its loop.");
try {
// ft.get() sẽ chặn luồng chính cho đến khi call() hoàn thành và trả về kết quả
int result = taskFuture.get();
System.out.println("Result from computation: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
FutureTask đóng vai trò quan trọng ở đây, nó vừa là một Runnable (cho phép nó được truyền vào constructor của Thread) vừa là một Future (cho phép lấy kết quả bằng phương thức get()). Phương thức get() của FutureTask sẽ chặn luồng gọi nó cho đến khi phương thức call() của Callable hoàn thành và trả về kết quả.
Lưu ý quan trọng: Dù sử dụng phương pháp nào, phương thức start() của một đối tượng Thread chỉ được gọi một lần duy nhất. Gọi start() lần thứ hai sẽ gây ra một IllegalThreadStateException.
3. Chuyển đổi trạng thái Sẵn sàng, Đang chạy và Kết thúc
- Sẵn sàng sang Đang chạy: Xảy ra khi bộ lập lịch CPU phân bổ thời gian xử lý cho luồng.
-
Đang chạy sang Sẵn sàng: Có thể xảy ra khi luồng chủ động gọi
Thread.yield()(tạm thời nhường quyền sử dụng CPU) hoặc khi hệ điều hành thu hồi quyền sử dụng CPU (thời gian CPU của luồng đã hết). -
Đang chạy sang Kết thúc: Xảy ra khi phương thức
run()(hoặccall()) hoàn thành việc thực thi, hoặc khi một ngoại lệ không được xử lý làm luồng bị dừng đột ngột.
Khi một luồng gọi yield(), nó chuyển từ trạng thái Đang chạy sang Sẵn sàng. Tuy nhiên, bộ lập lịch CPU có thể chọn lại chính luồng đó để chạy ngay lập tức, vì yield() chỉ là một gợi ý cho bộ lập lịch, không phải một lệnh bắt buộc.
Kiểm soát việc kết thúc luồng một cách an toàn
Để dừng một luồng một cách có kiểm soát, phương pháp phổ biến nhất là sử dụng một biến cờ (boolean flag) mà luồng sẽ kiểm tra định kỳ. Khi cờ được đặt, luồng sẽ thoát khỏi vòng lặp và kết thúc tự nhiên.
public class ThreadTerminationControl {
public static void main(String[] args) {
InterruptibleTask myTask = new InterruptibleTask();
Thread workerThread = new Thread(myTask, "ManagedWorker");
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " - Main operation: " + i);
if (i == 15) {
workerThread.start(); // Khởi động luồng con
}
if (i == 30) {
myTask.requestStop(); // Yêu cầu luồng con dừng
}
}
}
}
class InterruptibleTask implements Runnable {
private volatile boolean shouldStop = false; // volatile đảm bảo hiển thị đúng giữa các luồng
@Override
public void run() {
for (int j = 0; j < 100 && !shouldStop; j++) {
System.out.println(Thread.currentThread().getName() + " - Worker progress: " + j);
try {
Thread.sleep(50); // Mô phỏng công việc và kiểm tra cờ dừng
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " was interrupted.");
Thread.currentThread().interrupt(); // Thiết lập lại cờ ngắt
shouldStop = true; // Dừng luồng
}
}
System.out.println(Thread.currentThread().getName() + " finished or stopped.");
}
public void requestStop() {
this.shouldStop = true;
}
}
4. Trạng thái Bị chặn và kiểm soát luồng
Các phương thức dưới đây có thể khiến một luồng chuyển sang trạng thái Bị chặn:
4.1. join()
Phương thức join() cho phép một luồng chờ đợi một luồng khác hoàn thành công việc của nó trước khi tiếp tục. Nếu luồng A gọi B.join(), luồng A sẽ bị chặn cho đến khi luồng B kết thúc.
public class ThreadJoinExample {
public static void main(String[] args) {
SubProcessTask childJob = new SubProcessTask();
Thread childThread = new Thread(childJob, "ChildProcessor");
for (int mainIdx = 0; mainIdx < 50; mainIdx++) {
System.out.println(Thread.currentThread().getName() + " - Main Index: " + mainIdx);
if (mainIdx == 10) {
childThread.start();
try {
childThread.join(); // Luồng chính sẽ đợi 'childThread' hoàn thành
System.out.println("Main thread resumes after childThread finished.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Main thread interrupted while waiting.");
}
}
}
}
}
class SubProcessTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started its heavy lifting.");
for (int taskIdx = 0; taskIdx < 20; taskIdx++) {
System.out.println(Thread.currentThread().getName() + " - Task Index: " + taskIdx);
try {
Thread.sleep(100); // Giả lập công việc
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + " was interrupted during task.");
break;
}
}
System.out.println(Thread.currentThread().getName() + " completed.");
}
}
4.2. sleep()
Phương thức sleep() khiến luồng hiện tại tạm dừng thực thi trong một khoảng thời gian được chỉ định và chuyển sang trạng thái Bị chặn. Trong thời gian ngủ, luồng không tiêu thụ tài nguyên CPU và không thể được lập lịch chạy cho đến khi thời gian ngủ kết thúc. sleep() là một cách hiệu quả để tạm dừng luồng.
public class ThreadSleepExample {
public static void main(String[] args) {
QuickStartTask shortTask = new QuickStartTask();
Thread newThread = new Thread(shortTask, "FastWorker");
for (int mainStep = 0; mainStep < 30; mainStep++) {
System.out.println(Thread.currentThread().getName() + " - Main step: " + mainStep);
if (mainStep == 5) {
newThread.start();
try {
// Ngủ một thời gian ngắn để đảm bảo luồng mới có cơ hội được lập lịch
Thread.sleep(1);
System.out.println("Main thread briefly slept to let FastWorker run.");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Main thread interrupted during sleep.");
}
}
}
}
}
class QuickStartTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " has started immediately.");
for (int j = 0; j < 10; j++) {
System.out.println(Thread.currentThread().getName() + " - Internal count: " + j);
try {
Thread.sleep(5); // Ngủ một chút để mô phỏng công việc
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(Thread.currentThread().getName() + " was interrupted.");
break;
}
}
System.out.println(Thread.currentThread().getName() + " has completed.");
}
}
Việc gọi Thread.sleep(1) trong luồng chính sau khi khởi động một luồng mới có thể giúp tăng khả năng luồng mới được lập lịch chạy sớm hơn, vì luồng chính tạm thời nhường quyền sử dụng CPU.
4.3. Luồng nền (Daemon Thread)
Luồng nền, hay luồng "daemon", là những luồng cung cấp dịch vụ cho các luồng "foreground" (luồng chính). Ví dụ điển hình là bộ thu gom rác (Garbage Collector) của JVM. Điểm đặc biệt của luồng daemon là JVM sẽ tự động kết thúc nếu tất cả các luồng foreground đã kết thúc.
Bạn có thể đặt một luồng thành luồng daemon bằng cách gọi setDaemon(true) trước khi gọi start(). Phương thức isDaemon() có thể được dùng để kiểm tra.
public class DaemonThreadExample {
public static void main(String[] args) {
BackgroundTask daemonWorker = new BackgroundTask();
// Cần đặt là daemon trước khi start()
daemonWorker.setDaemon(true);
daemonWorker.start();
for (int i = 0; i < 50; i++) {
System.out.println("Main loop iteration: " + i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Main thread has finished. Daemon thread will soon terminate.");
}
}
class BackgroundTask extends Thread {
public void run() {
System.out.println(getName() + " started. Is daemon? " + isDaemon());
int count = 0;
while (true) { // Vòng lặp vô hạn, nhưng sẽ kết thúc khi main thread dừng
System.out.println(getName() + " - Worker count: " + count++);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println(getName() + " was interrupted and stopping.");
break;
}
}
System.out.println(getName() + " is stopping.");
}
}
Luồng chính mặc định là luồng foreground. Luồng con được tạo từ luồng foreground cũng mặc định là foreground. Luồng con được tạo từ luồng daemon sẽ là daemon.
4.4. Thay đổi độ ưu tiên của luồng (setPriority())
Mỗi luồng có một mức độ ưu tiên từ 1 (thấp nhất) đến 10 (cao nhất). Các luồng có độ ưu tiên cao hơn sẽ có nhiều cơ hội được bộ lập lịch CPU phân bổ thời gian chạy hơn. Mức độ ưu tiên mặc định là 5 (Thread.NORM_PRIORITY).
Bạn có thể thiết lập độ ưu tiên bằng setPriority(int level) và lấy bằng getPriority().
Lưu ý: Độ ưu tiên chỉ là một gợi ý cho hệ điều hành, không đảm bảo luồng ưu tiên cao sẽ luôn chạy trước hoặc chạy nhiều hơn. Việc lập lịch cụ thể vẫn phụ thuộc vào hệ điều hành.
public class ThreadPriorityExample {
public static void main(String[] args) {
HighPriorityWorker priorityThread = new HighPriorityWorker();
priorityThread.setName("HighP-Worker");
// Đặt độ ưu tiên tối đa
priorityThread.setPriority(Thread.MAX_PRIORITY);
priorityThread.start();
for (int i = 0; i < 50; i++) {
System.out.println(Thread.currentThread().getName() + " - Main iteration: " + i);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Main thread finished.");
}
}
class HighPriorityWorker extends Thread {
@Override
public void run() {
System.out.println(getName() + " started with priority " + getPriority());
for (int iteration = 0; iteration < 100; iteration++) {
System.out.println(getName() + " - Loop: " + iteration);
// Có thể thêm sleep ngắn để luồng khác có cơ hội chạy
// try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; }
}
System.out.println(getName() + " completed.");
}
}
4.5. Nhường quyền sử dụng CPU (yield())
Phương thức Thread.yield() là một gợi ý cho bộ lập lịch CPU rằng luồng hiện tại sẵn lòng từ bỏ quyền sử dụng CPU để các luồng khác có cơ hội chạy. Luồng gọi yield() sẽ chuyển từ trạng thái Đang chạy sang trạng thái Sẵn sàng. Bộ lập lịch sau đó sẽ chọn một luồng khác từ hàng đợi Sẵn sàng. Thông thường, bộ lập lịch có xu hướng chọn các luồng có độ ưu tiên bằng hoặc cao hơn luồng vừa gọi yield().
public class ThreadYieldExample {
public static void main(String[] args) {
FastProcessor fastTask = new FastProcessor();
fastTask.setName("FastThread");
fastTask.setPriority(Thread.MAX_PRIORITY); // Ưu tiên cao
SlowProcessor slowTask = new SlowProcessor();
slowTask.setName("SlowThread");
slowTask.setPriority(Thread.MIN_PRIORITY); // Ưu tiên thấp
fastTask.start();
slowTask.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " - Main operation: " + i);
if (i == 5) {
// Main thread nhường quyền, có thể giúp các luồng khác chạy
Thread.yield();
System.out.println("Main thread yielded its turn.");
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Main thread finished its work.");
}
}
class FastProcessor extends Thread {
@Override
public void run() {
System.out.println(getName() + " started (Priority: " + getPriority() + ")");
for (int i = 0; i < 50; i++) {
System.out.println(getName() + " -- Quick task " + i);
// Thỉnh thoảng nhường quyền
if (i % 10 == 0) Thread.yield();
}
System.out.println(getName() + " completed.");
}
}
class SlowProcessor extends Thread {
@Override
public void run() {
System.out.println(getName() + " started (Priority: " + getPriority() + ")");
for (int i = 0; i < 30; i++) {
System.out.println(getName() + " -- Long task " + i);
try {
Thread.sleep(5); // Mô phỏng công việc dài hơn
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println(getName() + " completed.");
}
}
5. Vấn đề an toàn luồng
Khi nhiều luồng truy cập và sửa đổi cùng một tài nguyên chia sẻ, các vấn đề về an toàn luồng có thể phát sinh, dẫn đến dữ liệu không nhất quán hoặc các lỗi khó gỡ. Để giải quyết vấn đề này, Java cung cấp các cơ chế đồng bộ hóa:
-
Từ khóa
synchronized: Dùng để bảo vệ các khối mã hoặc phương thức, đảm bảo chỉ có một luồng duy nhất có thể truy cập chúng tại một thời điểm. -
Lớp
ReentrantLock: Cung cấp khả năng khóa linh hoạt hơn so vớisynchronized, bao gồm khả năng thử khóa, khóa theo điều kiện và khóa với thời gian chờ.