IT Blog #10 | Java 21 Virtual Threads: Chương mới của lập trình bất đồng bộ

Java 21 Virtual Threads - A new chapter for asynchronous programming

Lập trình bất đồng bộ (Asynchronous Programming) dần trở thành “xương sống” của các hệ thống cần xử lý hàng nghìn yêu cầu đồng thời mà không lãng phí tài nguyên. Cách tiếp cận này cho phép ứng dụng tiếp tục thực hiện các tác vụ khác thay vì phải chờ đợi các thao tác I/O (đọc/ghi file, gọi network, truy vấn database…).

Trong Java, mô hình lập trình bất đồng bộ (Asynchronous Programming) đã trải qua nhiều giai đoạn phát triển.

(1)(2)

=>Những hạn chế của các mô hình async truyền thống như lãng phí thread, độ phức tạp cao hoặc khó debug đã đặt ra nhu cầu về một giải pháp đơn giản hơn.
Virtual Threads (thuộc Project Loom) ra đời nhằm cung cấp các thread nhẹ, có khả năng mở rộng cao mà không cần thay đổi phong cách viết code, kết hợp sự đơn giản của synchronous code với hiệu năng của async.

Virtual Thread là gì?

Virtual Thread được giới thiệu lần đầu trong Java 19 (JEP 425) dưới dạng preview và chính thức phát hành trong Java 21. Đây là một phần của Project Loom, với mục tiêu giúp lập trình bất đồng bộ đạt hiệu quả tương đương lập trình đồng bộ mà không làm tăng độ phức tạp của code.

So sánh Traditional Thread và Virtual Thread

Traditional Thread

(3)

Trước đây, chúng ta chỉ quen thuộc với một loại thread: Traditional Thread (còn gọi là platform thread hoặc OS thread).

Khi một yêu cầu được gửi đến để xử lý, JVM sẽ tạo một Java thread và gửi thông tin (như stack size, thread priority) cho hệ điều hành để thiết lập ánh xạ 1:1 với native thread. Sau khi kết nối hoàn tất, Java thread đóng vai trò như một giao diện nhận các thao tác và chuyển chúng cho native thread thực thi. Việc quản lý và scheduling hoàn toàn do hệ điều hành đảm nhiệm, giúp JVM vận hành tương đối đơn giản.

Tuy nhiên, việc tạo native thread trong OS có nhiều hạn chế và tiêu tốn tài nguyên. Theo mặc định, mỗi thread tiêu tốn khoảng ~1MB bộ nhớ.

(4)

Virtual Thread

Virtual Thread đánh dấu một cột mốc mới: việc scheduling và thực thi tác vụ được JVM quản lý thay vì OS như trước đây. Virtual thread không còn gắn trực tiếp 1:1 với native thread. Thay vào đó, carrier thread mới là thread được liên kết với native thread.

Khi bắt đầu thực thi, virtual thread sẽ được “mount” vào carrier thread. Cơ chế này được hỗ trợ bởi một thành phần trong JVM gọi là continuation, giúp lưu lại điểm bắt đầu. Virtual thread sau đó chạy trên carrier thread giống như native thread.

Khi virtual thread gặp các thao tác chờ I/O, carrier thread sẽ được unmount khỏi virtual thread. Trạng thái của virtual thread được lưu trong một continuation object trên Java heap, sẵn sàng được resume khi cần thiết.

Các đặc điểm chính của Virtual Thread

Virtual Threads rất nhẹ (Lightweight)

Virtual Threads (Project Loom) được thiết kế cực kỳ nhẹ — bạn có thể tạo hàng triệu virtual thread mà không tiêu tốn tài nguyên như traditional thread. Lý do là virtual thread không chiếm riêng một OS thread, mà chỉ “mượn” carrier thread khi cần thực thi.

Virtual Threads được thiết kế cho các tác vụ blocking

Các tác vụ như gọi API, truy vấn database, đọc/ghi file… thường không sử dụng CPU liên tục, mà dành phần lớn thời gian để chờ phản hồi bên ngoài. Với traditional thread, ngay cả khi chờ, thread vẫn bị chiếm dụng — dẫn đến lãng phí tài nguyên và nguy cơ cạn thread khi có nhiều request đồng thời.

Virtual thread hoạt động khác khi gặp blocking I/O (InputStream.read(), Socket.read()…), virtual thread sẽ tự suspend và giải phóng carrier thread để thread khác sử dụng. Kết quả là:

  • Throughput cao hơn cho các tác vụ I/O-bound
  • Sử dụng tài nguyên hiệu quả hơn (ít native thread, ít context switching)

Tuy nhiên, virtual thread không mang lại lợi ích cho CPU-bound tasks vì:

  • Không tăng song song thực sự (vẫn phụ thuộc số carrier thread)
  • Không tạo thêm CPU core
  • CPU-bound tasks vẫn cạnh tranh CPU như cũ

Virtual Threads mang hiệu năng async vào code synchronous

Trước đây, để xây dựng hệ thống scalable trong Java, chúng ta phải dùng: Callback (dễ rơi vào callback hell); CompletableFuture; Reactive streams (Project Reactor, RxJava…). Những mô hình này khó đọc, khó debug và khó bảo trì, đặc biệt với logic nghiệp vụ phức tạp.

Chúng cho phép developer viết code tuần tự, dễ đọc với các cấu trúc quen thuộc như try/catch, for, nhưng JVM sẽ tự động suspend/resume virtual thread khi gặp blocking call. (e.g., I/O, socket read, database access).

Điều này cho phép hệ thống đạt được hành vi non-blocking dù code vẫn trông và vận hành như blocking.
Nói cách khác, bạn vẫn có thể viết code theo phong cách đơn giản, tuần tự (synchronous), nhưng hệ thống vẫn có khả năng mở rộng cao như khi sử dụng non-blocking I/O.

Nhờ virtual threads có chi phí tạo và quản lý rất thấp, bạn có thể khởi tạo hàng nghìn, thậm chí hàng triệu thread mà không làm quá tải hệ thống — điều gần như không khả thi với platform threads do các giới hạn ở cấp độ hệ điều hành.

Compare between Traditional Thread and Virtual Thread

(5)

Virtual thread không thay thế traditional thread, mà bổ trợ để tối ưu các workload I/O-bound (HTTP, DB backcall…).

Một số điểm cần lưu ý

  • Chi phí thấp: Context switch rẻ, có thể tạo hàng triệu virtual thread
  • Tương thích ngược: Không cần thay đổi style code, chỉ cần thay Thread.start() bằng Thread.ofVirtual()
  • Throughput cao: Benchmark cho thấy hiệu suất vượt trội so với traditional thread

Demo
Mục tiêu: Minh họa hiệu quả của virtual thread trong môi trường high-concurrency với blocking I/O
Công cụ và thiết lập
So sánh: Virtual Threads vs Platform Threads (Thread usage, execution time).
Stack: JDK 21

Chúng tôi sẽ minh họa sự khác biệt về hiệu năng giữa traditional (platform) threads và virtual threads thông qua một đoạn code đơn giản. Bài kiểm thử bao gồm:

  • Tạo 10.000 thread cho cả Virtual Threads và Platform Threads, với thời gian chờ I/O mô phỏng là 1 giây.
  • Tạo 100.000 thread cho cả Virtual Threads và Platform Threads, với thời gian chờ I/O mô phỏng là 1 giây.
  • Mô phỏng cả tác vụ CPU-bound và I/O-bound để có cái nhìn so sánh toàn diện.
(5)
(6)
(7)
(8)
(9)

Kết quả benchmark so sánh hiệu năng giữa virtual threads và platform threads trên cả hai loại workload I/O-bound và CPU-bound, được thực hiện trên hệ thống Windows 11 với CPU 16 nhân.
Dưới đây là phân tích hiệu năng chi tiết và có cấu trúc:

Phân tích hiệu năng (Performance Matrix)

1750318908223

Những nhận định chính (Key Observations)

  1. Virtual Threads vượt trội rõ rệt trong các tác vụ I/O
  • 10.000 tác vụ I/O: Virtual Threads hoàn thành trong 1.449 ms, trong khi Platform Threads mất 3.963 ms (nhanh hơn 2,7 lần).
  • 100.000 tác vụ I/O: Virtual Threads hoàn thành trong 4.200 ms, trong khi Platform Threads mất 35.271 ms (nhanh hơn 8,4 lần).
  • Virtual Threads không giữ OS thread trong trạng thái chờ khi thực hiện I/O. Thay vì để OS thread bị idle, virtual threads chủ động nhường và resume một cách hiệu quả, cho phép JVM xử lý concurrency với chi phí overhead rất thấp. Ngược lại, platform threads gặp phải tranh chấp thread (thread contention) và độ trễ scheduling khi mức độ concurrency cao.
  1. Virtual Threads mở rộng (scale) rất tốt ở mức độ concurrency cao
    PeakThreadCount với 100.000 tác vụ I/O:
  • Virtual Threads: 31 (gần như không thay đổi so với trạng thái idle)
  • Platform Threads: 3.786 (tiệm cận giới hạn của OS)

TotalStartedThreadCount với 100.000 tác vụ I/O:

  • Virtual Threads: 33 (rất thấp)
  • Platform Threads: 100.014 (ánh xạ 1:1 cho mỗi task)
  • Virtual threads có trọng lượng nhẹ và được JVM quản lý, không phụ thuộc trực tiếp vào OS. Chúng tái sử dụng một pool nhỏ các carrier threads, trong khi platform threads tiêu thụ tài nguyên native của hệ điều hành, dẫn đến hiệu quả giảm dần khi scale lớn.
  1. Virtual Threads giúp giảm đáng kể overhead CPU của hệ thống

100.000 tác vụ I/O:

  • Virtual Threads: 2,07% System CPU
  • Platform Threads: 9,52% System CPU
  • Virtual threads giảm đáng kể chi phí scheduling ở kernel vì không phụ thuộc vào quản lý thread của OS.
    Trong khi đó, platform threads buộc OS phải xử lý hàng nghìn context switch, làm tăng tải hệ thống.
  1. Đánh đổi về bộ nhớ: Virtual Threads dùng nhiều hơn nhưng scale tốt hơn
  • 100.000 tác vụ I/O:
  • Virtual Threads: 177 MB
  • Platform Threads: 38 MB
  • Virtual threads sử dụng nhiều bộ nhớ hơn cho mỗi thread, nhưng tránh được hiện tượng chậm dần theo cấp số nhân như platform threads.
    Sự đánh đổi này là hoàn toàn hợp lý - tăng tốc 8,4 lần ở 100.000 tác vụ giá trị hơn nhiều so với việc tiết kiệm bộ nhớ.

Kết luận

Virtual threads mang lại hiệu năng vượt trội cho các workload I/O-bound, rất phù hợp cho việc xử lý HTTP requests và database calls.
Ngược lại, với các workload CPU-bound như tính toán nặng, thuật toán phức tạp hay machine learning, traditional threads vẫn hiệu quả hơn.

  • Ngoài hiệu năng, virtual threads còn có nhiều ưu điểm:
  • Dễ sử dụng
  • Thay đổi code tối thiểu
  • Viết code synchronous nhưng đạt hiệu năng như async
  • Tương thích với các thư viện synchronous hiện có (JDBC, Servlet…)

📌 Lưu ý: Dự án cần nâng cấp lên Java 21 trở lên để sử dụng virtual threads.

Được viết bởi Phat Le & Mr. Phu Ngo - Bộ phận CNTT tại Home Credit Việt Nam