CodeGym /Các khóa học /JAVA 25 SELF /Tạo luồng ảo: Thread.ofVirtual().start()

Tạo luồng ảo: Thread.ofVirtual().start()

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

1. Cách tạo luồng ảo trong thực tế

Đã đến lúc chuyển từ lý thuyết sang thực hành! Bạn đã biết cách tạo luồng theo cách cổ điển:

Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();

Hoặc ngắn gọn hơn:

new Thread(() -> System.out.println("Hi!")).start();

Giờ đây với Java 21, chúng ta có cách mới:

Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));

hoặc rõ ràng hơn:

Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));

Khác nhau ở đâu?

  • Thread.ofVirtual().start(...) tạo luồng ảo (Virtual Thread) do JVM quản lý, không phải hệ điều hành.
  • Thread.ofPlatform().start(...) (hoặc new Thread(...)) — luồng nền tảng cổ điển như trước đây.

Vì sao điều này quan trọng?

Bạn có thể tạo hàng chục nghìn luồng ảo mà không lo OutOfMemoryError. Giờ đây, nếu bạn quyết định xử lý một triệu yêu cầu — Java sẽ nói: “Không vấn đề, cứ tiếp đi!”

2. Cú pháp tạo luồng ảo

Ví dụ cơ bản:

public class VirtualThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.ofVirtual().start(() -> {
            System.out.println("Xin chào từ luồng ảo! Luồng: " + Thread.currentThread());
        });
        // Chờ luồng kết thúc (để main không kết thúc sớm hơn)
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Điều gì đang diễn ra?

  • Chúng ta tạo luồng ảo qua Thread.ofVirtual().start(...).
  • Bên trong luồng là hành động đơn giản: in thông điệp.
  • Cuối cùng gọi thread.join() để luồng chính chờ luồng ảo kết thúc (nếu không chương trình có thể kết thúc trước khi luồng kịp in ra).

Lưu ý:
Luồng ảo trông và hành xử gần như giống hệt luồng thường, nhưng bên trong là “ma thuật” của JVM!

3. Tạo hàng loạt luồng ảo: sức mạnh Loom trong thực tiễn

Giờ hãy thử điều mà với luồng thường sẽ rủi ro (hoặc không thể): tạo 10_000 luồng ảo, mỗi luồng in ra số thứ tự của mình.

public class VirtualThreadMassive {
    public static void main(String[] args) throws InterruptedException {
        int N = 10_000;
        Thread[] threads = new Thread[N];

        for (int i = 0; i < N; i++) {
            int threadNum = i;
            threads[i] = Thread.ofVirtual().start(() -> {
                System.out.println("Luồng ảo #" + threadNum + " đang chạy!");
            });
        }

        // Chờ tất cả các luồng kết thúc
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("Tất cả các luồng ảo đã hoàn tất!");
    }
}
  • Với luồng thường (new Thread(...)) đoạn mã này gần như chắc chắn sẽ “làm sập” chương trình của bạn với OutOfMemoryError.
  • Với luồng ảo — đây là chế độ hoạt động tiêu chuẩn! JVM dễ dàng xử lý hàng nghìn, hàng chục nghìn luồng.

Nhân tiện, nếu bạn thấy 10_000 là nhiều, hãy thử 100_000 hoặc thậm chí 1_000_000. Trên máy hiện đại, JVM vẫn xử lý được nếu các luồng của bạn làm việc đơn giản hoặc đang chờ I/O.

4. Runnable và biểu thức lambda: cách truyền mã cho luồng ảo

Luồng ảo nhận tác vụ giống như luồng thường: thông qua giao diện Runnable. Nghĩa là bạn có thể truyền cả biểu thức lambda, tham chiếu phương thức, hoặc bất kỳ đối tượng nào triển khai Runnable.

Ví dụ với lambda:

Thread.ofVirtual().start(() -> System.out.println("Lambda trong luồng ảo!"));

Ví dụ với method:

public class TaskRunner {
    public static void main(String[] args) {
        Thread.ofVirtual().start(TaskRunner::doWork);
    }

    static void doWork() {
        System.out.println("Đang chạy trong luồng ảo: " + Thread.currentThread());
    }
}

Ví dụ với lớp ẩn danh:

Thread.ofVirtual().start(new Runnable() {
    @Override
    public void run() {
        System.out.println("Lớp ẩn danh trong luồng ảo!");
    }
});

Kết luận:
Mọi thứ hoạt động với luồng thường đều hoạt động với luồng ảo — chỉ là giờ đây “nhẹ và nhanh” hơn.

5. So sánh với ExecutorService: cách cũ và mới

ExecutorService cổ điển

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("Tác vụ #" + taskNum + " đang chạy");
    });
}
executor.shutdown();

Vấn đề:
Nếu có quá nhiều tác vụ mà số luồng ít — các tác vụ sẽ phải chờ trong hàng đợi. Nếu quá nhiều luồng — chương trình sẽ “đuối” do thiếu tài nguyên.

Cách mới: Executor dựa trên luồng ảo

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("Tác vụ ảo #" + taskNum);
    });
}
executor.shutdown();

Điều gì xảy ra?

  • Mỗi tác vụ sẽ có một luồng ảo riêng.
  • JVM tự quản lý lập lịch cho chúng mà không làm quá tải hệ thống.
  • Không cần giới hạn kích thước pool — luồng ảo “gần như miễn phí”.

Khi nào sử dụng Executor dựa trên luồng ảo?

  • Khi bạn có lượng lớn tác vụ và không muốn bận tâm về kích thước pool.
  • Khi các tác vụ độc lập và có thể chạy song song.
  • Khi muốn sự đơn giản: không cần quản lý luồng thủ công và dễ tích hợp vào kiến trúc đang dùng ExecutorService (ví dụ web server, bộ xử lý tác vụ, v.v.).

6. Lời khuyên thực tế: khi nào dùng cái gì

Khi nào dùng trực tiếp Thread.ofVirtual().start()?

  • Khi cần tạo luồng riêng cho một tác vụ duy nhất (ví dụ test, demo hoặc thử nghiệm đơn giản).
  • Khi số lượng luồng ít và bạn muốn tự quản lý chúng.

Khi nào dùng Executors.newVirtualThreadPerTaskExecutor()?

  • Khi cần chạy hàng loạt tác vụ (ví dụ xử lý số lượng lớn yêu cầu, tệp, kết nối mạng).
  • Khi các tác vụ độc lập và không cần phối hợp với nhau.
  • Khi muốn tích hợp luồng ảo vào kiến trúc hiện có, nơi đã dùng ExecutorService (ví dụ web server, bộ xử lý tác vụ, v.v.).

Gợi ý:
Nếu chưa chắc — hãy bắt đầu với Executor dựa trên luồng ảo. Đây là cách hiện đại và linh hoạt nhất.

7. Xử lý ngoại lệ trong luồng ảo

Với try-catch, luồng ảo cũng như luồng thường. Nếu ngoại lệ xảy ra bên trong Runnable của bạn, nó sẽ không “làm sập” cả JVM, mà chỉ kết thúc luồng đó với lỗi.

Ví dụ:

Thread t = Thread.ofVirtual().start(() -> {
    throw new RuntimeException("Đã có sự cố!");
});
try {
    t.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Luồng chính tiếp tục chạy.");

Trong ExecutorService:
Nếu gửi tác vụ qua submit, bạn có thể lấy kết quả qua Future, và ngoại lệ sẽ được ném ra khi gọi get():

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
    throw new RuntimeException("Lỗi trong tác vụ ảo");
});
try {
    f.get();
} catch (ExecutionException e) {
    System.out.println("Bắt được lỗi từ luồng ảo: " + e.getCause());
}
executor.shutdown();

8. Các lỗi thường gặp khi tạo luồng ảo

Lỗi 1: Nhầm lẫn luồng ảo và luồng nền tảng. Nếu bạn tạo luồng qua new Thread(...) hoặc Thread.ofPlatform(), đó không phải luồng ảo. Chỉ Thread.ofVirtual().start(...) hoặc các phương thức trong Executors mới tạo Virtual Threads thực sự.

Lỗi 2: Kỳ vọng tăng tốc cho tác vụ tính toán nặng. Luồng ảo không tăng tốc các tác vụ tiêu tốn CPU (CPU-bound). Nếu bạn có một triệu luồng, mỗi luồng tính số Pi đến chữ số thứ một triệu — JVM không thể “tăng tốc” việc tính toán, nó chỉ chuyển đổi qua lại giữa các luồng.

Lỗi 3: Giữ tài nguyên (ví dụ kết nối cơ sở dữ liệu) riêng cho mỗi luồng. Nếu bạn tạo một triệu luồng ảo nhưng mỗi luồng cần một kết nối DB riêng — DB sẽ không chịu nổi. Luồng ảo phù hợp cho tác vụ mà phần lớn thời gian là chờ đợi (I/O), không phải làm việc với tài nguyên bên ngoài bị giới hạn.

Lỗi 4: Không chờ luồng kết thúc khi điều đó quan trọng. Nếu luồng chính kết thúc trước luồng ảo — chương trình có thể dừng mà chưa có kết quả. Hãy dùng join() hoặc ExecutorService với shutdown()awaitTermination().

Lỗi 5: Dùng thư viện lỗi thời, không tương thích với luồng ảo. Một số thư viện bên thứ ba có thể chặn luồng ở mức hệ điều hành hoặc dùng đồng bộ hóa native, làm giảm hiệu quả của luồng ảo. Luôn kiểm tra tính tương thích.

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