Trong Java, một Thread đại diện cho một luồng thực thi độc lập trong chương trình, cho phép thực hiện nhiều công việc đồng thời (multithreading). Java cung cấp nhiều cách khác nhau để tạo một Thread, mỗi cách đều có các ưu điểm riêng biệt. Trong bài viết này, chúng ta sẽ xem xét các cách tạo một Thread và thảo luận về sự khác biệt giữa chúng. Cuối cùng, tôi sẽ chia sẻ quan điểm về cách tạo Thread mà tôi thích và lý do tại sao.

Cách 1: Kế thừa từ lớp Thread

Giới thiệu

Cách đơn giản nhất để tạo một Thread là kế thừa từ lớp Thread và ghi đè phương thức run() để định nghĩa hành vi của thread. Sau đó, bạn có thể tạo một đối tượng của lớp con và gọi phương thức start() để bắt đầu luồng thực thi.

Ví dụ

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}

MyThread thread = new MyThread();
thread.start();

Trong ví dụ trên, chúng ta tạo lớp MyThread kế thừa từ Thread và định nghĩa hành vi trong phương thức run(). Phương thức start() được gọi để bắt đầu thực thi thread.

Ưu và nhược điểm

Ưu điểm:

  • Cách tiếp cận đơn giản, dễ hiểu và dễ sử dụng.
  • Có thể sử dụng trực tiếp các phương thức của lớp Thread như sleep(), join(), interrupt().

Nhược điểm:

  • Không thể kế thừa từ lớp khác vì Java chỉ hỗ trợ kế thừa đơn (single inheritance). Nếu lớp đã kế thừa từ một lớp khác, bạn không thể sử dụng cách này.
  • Giới hạn về khả năng tái sử dụng mã, vì hành vi của thread được gắn trực tiếp với lớp Thread.

Cách 2: Triển khai giao diện Runnable

Giới thiệu

Một cách khác để tạo Thread là triển khai giao diện Runnable. Bạn sẽ cần cung cấp định nghĩa cho phương thức run() và truyền một đối tượng Runnable vào đối tượng Thread để thực thi luồng.

Ví dụ

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}

Thread thread = new Thread(new MyRunnable());
thread.start();

Trong ví dụ này, chúng ta tạo lớp MyRunnable triển khai giao diện Runnable và định nghĩa hành vi trong phương thức run(). Sau đó, chúng ta truyền đối tượng MyRunnable vào lớp Thread và gọi phương thức start().

Ưu và nhược điểm

Ưu điểm:

  • Cách tiếp cận linh hoạt hơn vì bạn có thể kế thừa từ các lớp khác trong khi vẫn triển khai Runnable.
  • Khả năng tái sử dụng cao, bạn có thể tách biệt logic của thread và sử dụng lại ở nhiều nơi khác nhau.

Nhược điểm:

  • Không trực tiếp kế thừa từ lớp Thread, nên phải sử dụng đối tượng Thread để khởi chạy thread.

Cách 3: Sử dụng biểu thức Lambda (từ Java 8 trở đi)

Giới thiệu

Từ Java 8, với sự ra đời của biểu thức lambda, bạn có thể tạo Thread một cách gọn gàng hơn khi triển khai giao diện Runnable. Thay vì định nghĩa một lớp riêng biệt, bạn có thể truyền trực tiếp một biểu thức lambda vào Thread.

Ví dụ

Thread thread = new Thread(() -> {
    System.out.println("Thread is running");
});
thread.start();

Biểu thức lambda cho phép bạn tạo Thread một cách ngắn gọn và trực quan hơn, đặc biệt là trong những tình huống đơn giản.

Ưu và nhược điểm

Ưu điểm:

  • Cách tiếp cận gọn gàng và dễ đọc hơn so với việc triển khai giao diện Runnable truyền thống.
  • Giảm thiểu mã nguồn boilerplate (lặp đi lặp lại).

Nhược điểm:

  • Chỉ có thể sử dụng từ Java 8 trở đi.
  • Dễ làm mất tính rõ ràng của mã trong các tình huống phức tạp.

Cách 4: Sử dụng CallableFuture

Giới thiệu

Nếu bạn cần kết quả trả về từ thread hoặc cần xử lý ngoại lệ, giao diện Callable là lựa chọn tốt hơn so với Runnable. Callable cho phép trả về kết quả và ném ngoại lệ. Sau đó, bạn có thể sử dụng ExecutorService để quản lý thread và lấy kết quả bằng Future.

Ví dụ

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Thread result";
    }
}

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());

String result = future.get();  // Lấy kết quả từ thread
System.out.println(result);

executor.shutdown();

Ở đây, Callable giúp bạn trả về kết quả từ thread thông qua Future. Phương thức call() thay thế run() trong Runnable và có thể ném ngoại lệ.

Ưu và nhược điểm

Ưu điểm:

  • Cho phép trả về kết quả từ thread.
  • Có thể xử lý ngoại lệ trong quá trình thực thi.
  • Thích hợp cho các tác vụ cần tính toán kết quả.

Nhược điểm:

  • Phức tạp hơn so với Runnable trong việc quản lý thread.
  • Cần sử dụng thêm các lớp như ExecutorServiceFuture.

Cách yêu thích của tôi

Cách tôi thích nhất để tạo Thread trong Java là triển khai giao diện Runnable.

Lý do

  1. Linh hoạt: Việc sử dụng Runnable cho phép tôi tách biệt hành vi của thread khỏi lớp cụ thể, giúp mã nguồn dễ tái sử dụng hơn. Ngoài ra, tôi có thể kế thừa từ các lớp khác nếu cần, mà không bị giới hạn bởi việc chỉ có thể kế thừa một lớp trong Java.
  2. Rõ ràng: Mặc dù lambda là một cách viết gọn gàng, nhưng trong các dự án lớn, tôi ưu tiên sự rõ ràng. Việc tạo một lớp triển khai Runnable giúp tôi và đồng đội dễ dàng theo dõi và duy trì mã nguồn hơn, đặc biệt khi xử lý các logic phức tạp trong thread.
  3. Tính tương thích: Runnable có tính tương thích cao, dễ sử dụng và dễ kết hợp với các framework hoặc API khác trong Java. Điều này giúp tôi linh hoạt hơn khi cần tích hợp với các công cụ quản lý thread khác như ExecutorService.

Tóm lại, Java cung cấp nhiều cách để tạo Thread, bao gồm kế thừa từ Thread, triển khai Runnable, sử dụng lambda, và Callable với Future. Cá nhân tôi ưa thích sử dụng Runnable vì tính linh hoạt, khả năng tái sử dụng, và tính rõ ràng mà nó mang lại trong việc phát triển phần mềm.