CodeGym /Các khóa học /JAVA 25 SELF /Khởi chạy luồng: Thread và Runnable, cú pháp

Khởi chạy luồng: Thread và Runnable, cú pháp

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

1. Lớp Thread: luồng đầu tiên trong Java

Trong Java, mỗi luồng thực thi được biểu diễn bởi một đối tượng của lớp Thread. Lớp này — như một nhạc trưởng: chính nó điều khiển việc khởi chạy, dừng và vòng đời của luồng.

Để khởi chạy một luồng, bạn cần:

  1. Tạo một đối tượng của lớp Thread (hoặc lớp dẫn xuất của nó).
  2. Gọi phương thức start() trên đối tượng đó.

Hãy cùng xem qua thực tế.

Ví dụ 1. Kế thừa từ Thread

Cách trực tiếp nhất để tạo luồng là kế thừa lớp của bạn từ Thread và ghi đè (override) phương thức run() của nó. Mọi thứ bạn viết bên trong run() sẽ chạy trong một luồng riêng.

// Luồng đơn giản thông qua kế thừa
public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("Xin chào từ luồng! Tên tôi là: " + getName());
    }
}

public class Main {
    public static void main(String[] args) {
        HelloThread thread = new HelloThread(); // tạo đối tượng luồng
        thread.start(); // khởi chạy luồng
        System.out.println("Luồng chính kết thúc công việc.");
    }
}

Thực sự thì chuyện gì xảy ra?
Khi bạn gọi thread.start(), Java tạo một luồng mới và trong luồng đó nó chạy phương thức run(). Trong khi đó, luồng chính (luồng bắt đầu từ main) không chờ mà tiếp tục chạy song song.

Và điều quan trọng cần lưu ý: đừng nhầm start() với việc gọi trực tiếp run(). Nếu viết thread.run(), sẽ không có luồng mới nào được tạo — đó chỉ là một phương thức bình thường, chạy trong cùng luồng nơi bạn gọi nó. Đa luồng thực sự chỉ bắt đầu với start().

getName() là gì?

Phương thức getName() trả về tên của luồng. Mặc định, Java đặt tên cho các luồng theo dạng "Thread-0", "Thread-1", v.v. Điều này rất tiện cho việc gỡ lỗi.

2. Interface Runnable: cách tốt nhất trong hầu hết trường hợp

Java — một ngôn ngữ ưa thích sự linh hoạt. Lớp Thread đã kế thừa từ Object, và trong Java không có đa kế thừa lớp. Nếu bạn kế thừa lớp của mình từ Thread, bạn sẽ không thể kế thừa nó từ lớp khác nữa (ví dụ, nếu bạn có lớp Car và muốn biến nó thành một luồng — bạn sẽ phải lựa chọn).

Giải pháp: đưa logic của luồng vào một đối tượng riêng cài đặt interface Runnable. Đây là một interface với một phương thức run(). Sau đó bạn tạo đối tượng lớp Thread, truyền Runnable của bạn cho nó — và khởi chạy luồng.

Ví dụ 2. Lớp với Runnable

// Lớp cài đặt Runnable
public class HelloRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Xin chào từ Runnable! Luồng: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Runnable runnable = new HelloRunnable();
        Thread thread = new Thread(runnable); // truyền Runnable vào Thread
        thread.start();
        System.out.println("Luồng chính kết thúc công việc.");
    }
}

Ở đây chúng ta tạo lớp HelloRunnable cài đặt interface Runnable. Trong phương thức main, một đối tượng của lớp này được tạo ra và truyền vào constructor của Thread. Sau đó luồng được khởi chạy bằng cách gọi start().

Cũng đáng nhắc tới phương thức Thread.currentThread(). Đây là phương thức tĩnh cho phép lấy đối tượng luồng hiện tại, qua đó hiểu được mã của chúng ta đang chạy ở đâu.

Tại sao cách này tốt hơn?

  • Lớp của bạn có thể kế thừa từ bất kỳ lớp nào khác (không chỉ từ Thread).
  • Có thể tái sử dụng cùng một Runnable để chạy trong nhiều luồng.
  • Mã trở nên linh hoạt và sạch sẽ hơn.

3. Cú pháp: lớp ẩn danh và biểu thức lambda

Java không đứng yên! Từ Java 8, chúng ta có thể viết còn ngắn gọn và tiện hơn.

Ví dụ 3. Lớp ẩn danh

Đôi khi bạn không muốn tạo tệp riêng cho một lớp chỉ dùng một lần. Có thể khai báo ngay tại nơi sử dụng — đó gọi là lớp ẩn danh.

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable ẩn danh! Luồng: " + Thread.currentThread().getName());
            }
        });
        thread.start();
        System.out.println("Luồng chính kết thúc công việc.");
    }
}

Ví dụ 4. Biểu thức lambda (Java 8+)

Interface Runnable là interface hàm (chỉ có một phương thức trừu tượng). Vì vậy có thể dùng biểu thức lambda:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Luồng lambda! Luồng: " + Thread.currentThread().getName());
        });
        thread.start();
        System.out.println("Luồng chính kết thúc công việc.");
    }
}

Tóm lại:
Runnable được cài đặt “tại chỗ”, và biểu thức lambda cung cấp triển khai cho phương thức run().

4. Khởi chạy nhiều luồng

Có thể khởi chạy bao nhiêu luồng tuỳ thích (gần như vậy — còn phụ thuộc tài nguyên máy tính).

Ví dụ 5. Khởi chạy nhiều luồng

public class Main {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            int threadNumber = i; // nhất định phải sao chép biến cho lambda!
            Thread thread = new Thread(() -> {
                System.out.println("Luồng #" + threadNumber + ": xin chào!");
            });
            thread.start();
        }
        System.out.println("Luồng chính kết thúc công việc.");
    }
}

Kết quả in ra có thể khác nhau!
Các luồng chạy song song — việc ai in ra trước phụ thuộc vào hệ điều hành và bộ lập lịch luồng.

5. Vòng đời của luồng

Hiểu luồng “sống” như thế nào rất quan trọng cho việc gỡ lỗi và sử dụng luồng đúng cách.

Các trạng thái chính của luồng

  • NEW — luồng đã được tạo nhưng chưa khởi chạy (new Thread(...)).
  • RUNNABLE — luồng đã được khởi chạy (start()), có thể đang thực thi.
  • TERMINATED — luồng đã kết thúc thực thi (phương thức run() đã hoàn tất).

Điều gì xảy ra khi gọi start() và run()

  • start() — tạo một luồng mới và gọi phương thức run() của nó trong luồng mới đó.
  • run() — là phương thức bình thường; nếu gọi trực tiếp, nó sẽ chạy trong luồng hiện tại (không tạo luồng mới!).

Ví dụ:

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("run() từ luồng: " + Thread.currentThread().getName()));
        thread.run();   // ĐƯỢC GỌI TRONG LUỒNG CHÍNH!
        thread.start(); // ĐƯỢC GỌI TRONG MỘT LUỒNG RIÊNG!
    }
}

6. Thực hành: tạo một ứng dụng đa luồng đơn giản

Hãy tiếp tục phát triển ứng dụng học tập của chúng ta — giả sử ta viết một hệ thống logging đơn giản, nơi mỗi luồng ghi ra thông điệp của riêng nó.

public class LoggerTask implements Runnable {
    private final String message;

    public LoggerTask(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        System.out.println("[" + Thread.currentThread().getName() + "] Log: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Thread logger1 = new Thread(new LoggerTask("Khởi động hệ thống"));
        Thread logger2 = new Thread(new LoggerTask("Tải dữ liệu"));
        Thread logger3 = new Thread(new LoggerTask("Xử lý yêu cầu"));

        logger1.start();
        logger2.start();
        logger3.start();

        System.out.println("Luồng chính kết thúc công việc.");
    }
}

Chuyện gì đang xảy ra?
Ba luồng ghi thông điệp của mình ra console song song. Thứ tự thông điệp không được đảm bảo — đó chính là bản chất của đa luồng.

7. Những lưu ý hữu ích

Bảng: so sánh các cách tạo luồng

Cách Tính linh hoạt Tái sử dụng Có khuyến nghị không?
Kế thừa từ Thread Thấp Không Chỉ cho ví dụ đơn giản/học tập
Cài đặt Runnable Cao
Lớp ẩn danh Trung bình Không Cho các tác vụ dùng một lần
Biểu thức lambda Cao Không Có (Java 8+)

Sơ đồ: khởi chạy luồng hoạt động như thế nào

+----------------------+ 
|  main (luồng chính)  |
+----------------------+
           |
           v
+----------------------+
|  Thread thread = ... |
+----------------------+
           |
           v
+----------------------+
|  thread.start()      |  ← Luồng "thức dậy"
+----------------------+
           |
           v
+----------------------+
|  run() trong luồng mới|
+----------------------+
           |
           v
+----------------------+
|  Luồng đã kết thúc   |
+----------------------+

8. Lỗi thường gặp khi khởi chạy luồng

Lỗi số 1: Gọi run() thay vì start().
Một lỗi rất thường gặp của người mới — gọi trực tiếp phương thức run() trên đối tượng luồng. Khi đó sẽ không tạo ra luồng mới nào, phương thức chỉ chạy trong luồng hiện tại (luồng chính). Đúng: luôn dùng start() để khởi chạy luồng.

Lỗi số 2: Khởi chạy lại cùng một luồng.
Không thể gọi start() trên cùng một đối tượng Thread nhiều hơn một lần — điều này sẽ ném IllegalThreadStateException. Nếu bạn cần chạy lại nhiệm vụ, hãy tạo một đối tượng Thread mới.

Lỗi số 3: Thay đổi biến dùng chung mà không đồng bộ.
Nếu nhiều luồng cùng truy cập các biến chung, kết quả có thể khó lường (race condition). Về đồng bộ chúng ta sẽ nói sau, còn hiện tại — hãy thận trọng.

Lỗi số 4: Không chờ luồng kết thúc.
Nếu cần chờ luồng hoàn thành, hãy dùng phương thức join(). Nếu không, luồng chính có thể kết thúc trước các luồng con.

Lỗi số 5: Nhầm tên luồng với tên lớp.
Phương thức getName() trả về tên của luồng, không phải tên lớp. Khi gỡ lỗi, luôn hữu ích khi đặt tên luồng một cách tường minh qua constructor hoặc setName().

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION