logo

Lộ trình

Khóa học

Tài liệu

Mock Interview

Liên hệ

Quay lại
  • Trang chủ

    /

  • Tài liệu

    /

  • Java Virtual Thread: Cuộc cách mạng cho lập trình đồng thời
Tài liệu

Java Virtual Thread: Cuộc cách mạng cho lập trình đồng thời

Ronin Engineer

2 Tháng 8 2025

<p>By @wuan.580</p><p>Bạn đã bao giờ viết một ứng dụng xử lý hàng ngàn request cùng lúc, và cảm thấy như mình đang <em>chiến đấu với chính Java</em>?</p><p>Bạn từng dùng ThreadPoolExecutor và vắt óc cân chỉnh số lượng thread cho "vừa đủ dùng", tránh thiếu nhưng cũng không dám dư vì sợ <em>OutOfMemoryError</em>?</p><p>Bạn từng nhăn mặt khi phải viết những dòng code callback chằng chịt, chỉ để tránh block một luồng? Và rồi đau đầu gỡ bug vì stacktrace rối như tơ vò?</p><p>Nếu câu trả lời là "có", thì bạn không đơn độc. Và bạn cũng sắp có một giải pháp: Virtual Thread – một trong những bước nhảy vọt quan trọng nhất của Java trong thập kỷ qua.</p><p>Ra mắt như một phần của Project Loom, virtual thread mở ra một cách tiếp cận hoàn toàn mới: viết code đồng bộ như bình thường, nhưng hiệu năng gần như bất đồng bộ. Không còn callback hell, không cần reactive framework phức tạp, và đặc biệt là, bạn có thể tạo hàng triệu luồng mà JVM vẫn mỉm cười nhẹ nhàng.</p><p>Trong bài viết này, chúng ta sẽ khám phá:</p><ul><li>Virtual thread là gì và tại sao nó <em>thay đổi cuộc chơi</em>?</li><li>Cách JVM "ảo thuật" để luồng block không còn tốn kém.</li><li>Những khác biệt then chốt giữa virtual thread và platform thread.</li><li>Khi nào nên dùng virtual thread, và những tình huống cần cân nhắc.</li><li>Một số ví dụ thực tế: từ lý thuyết đến performance thực chiến.</li></ul><blockquote><em>Virtual thread không chỉ là một cải tiến về kỹ thuật, nó là lời tuyên bố: Java vẫn chưa già, và vẫn có thể hiện đại hóa một cách mạnh mẽ.</em></blockquote><h1 id="1-th%E1%BB%9Di-ti%E1%BB%81n-s%E1%BB%AD-tr%C6%B0%E1%BB%9Bc-virtual-thread-platform-thread-v%C3%A0-gi%E1%BA%A3i-ph%C3%A1p-n%E1%BB%ADa-v%E1%BB%9Di">1. Thời tiền sử trước Virtual Thread: Platform Thread và giải pháp nửa vời</h1><p>Trước khi Virtual Thread ra đời, Java đã có một hành trình dài loay hoay với bài toán xử lý đồng thời. Những gì chúng ta gọi là “platform thread” thực chất là một lớp bọc (wrapper) giữa JVM và OS thread – thứ sinh ra không phải để tạo ra hàng triệu luồng.</p><p>Trong phần này, chúng ta sẽ cùng nhìn lại các vấn đề đặc trưng của Platform Thread, và các giải pháp trước đây đã cố gắng khắc phục nó: thread pool, callback, reactive programming, ...</p><h2 id="11-platform-thread-%E2%80%93-c%C3%A1ch-m%C3%A0-jvm-l%C3%A0m-vi%E1%BB%87c-v%E1%BB%9Bi-h%E1%BB%87-%C4%91i%E1%BB%81u-h%C3%A0nh">1.1. Platform Thread – Cách mà JVM làm việc với hệ điều hành</h2><p>Trước khi bước vào thế giới của Virtual Thread, hãy dành chút thời gian để nhìn lại cách Java truyền thống xử lý luồng – thứ mà chúng ta gọi là Platform Thread.</p><figure class="kg-card kg-image-card"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfzhhD_vd9EXZci1sGZAYkR74aT3xDNqr0Qao4NpI6CEj7sFCF7EOuUeFfjTcb7gVZMxrDBGTKqnAAUnzqkZm7p2DGcK82F14RHAaGfkerupWGqvHvrJYziVTYpW0BTodLkoHTXdw?key=AN7_aS17-3rGf2aHU6nwVA" class="kg-image" alt="" loading="lazy" width="982" height="689"></figure><p>Hình minh họa trên cho thấy cấu trúc hoạt động của Platform Thread qua 3 tầng:</p><ul><li>JVM Layer: Mỗi Thread bạn tạo ra trong Java là một Platform Thread. JVM sẽ tạo một stack riêng cho từng thread và điều phối nó.</li><li>OS Layer: JVM <strong>ánh xạ 1:1 các Platform Thread với các native OS thread</strong> (dùng pthread trên Linux hoặc CreateThread trên Windows).</li><li>CPU Layer: Hệ điều hành sẽ lên lịch (schedule) các OS thread này chạy trên các CPU core thực sự.</li></ul><p><strong>Tại sao lại gọi là "Platform" Thread?</strong></p><p>Bởi vì mỗi java.lang.Thread sẽ được nền tảng hệ điều hành (platform) cung cấp tài nguyên — bạn tạo một thread trong Java, và JVM phải “nhờ” OS cấp cho bạn một thread thực sự. </p><p>Điều này kéo theo một vài hệ quả sau:</p><h2 id="12-v%E1%BA%A5n-%C4%91%E1%BB%81-1-thread-qu%C3%A1-n%E1%BA%B7ng">1.2. Vấn đề 1: Thread quá "nặng"</h2><p>Mỗi thread trong Java (platform thread) là một OS thread, điều đó có nghĩa là:</p><ul><li>Tạo thread tốn chi phí vì JVM phải gọi native API để khởi tạo một OS thread.</li><li>Nếu bạn tạo 100.000 threads, bạn đang yêu cầu JVM sử dụng... 100GB RAM chỉ để giữ stack, nghe thôi đã thấy đốt tiền rồi :&gt;</li></ul><p>Mỗi thread chiếm <code>~ 1MB</code> bộ nhớ stack.</p><figure class="kg-card kg-image-card"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXf2gjZt3CFN6G9TKd08R7a0qpaURTCAQYc0qjY7a4jIdIJyJIOQ1wuTSwCxF173Qz-p1-Okp3OWDKYW6iYeBQ4AklOmmGcjtp__aqOZGeK751pbkGHrBpcYBoCF1tRcxHT8j8V_?key=AN7_aS17-3rGf2aHU6nwVA" class="kg-image" alt="" loading="lazy" width="1600" height="555"></figure><p>⇒ Và rõ ràng điều này không khả thi. Vì thế lập trình viên buộc phải dùng thread pool, và bắt đầu bước vào mê cung của các <code>ExecutorService, RejectedExecutionHandler, ThreadFactory, ...</code></p><ul><li><strong>Việc tạo thread tốn chi phí</strong>: Cần hệ điều hành cấp phát stack memory, context switching, register state, kernel resource...</li><li><strong>Quản lý thread số lượng lớn không hiệu quả</strong>: Khi bạn muốn xử lý 1 triệu request đồng thời? Không thể nào tạo 1 triệu OS thread!</li><li>Bài toán <strong>blocking càng nghiêm trọng hơn</strong>: Nếu thread đó bị block do I/O (gọi HTTP, đọc DB...), OS thread đó vẫn bị chiếm dụng, lãng phí tài nguyên.</li></ul><h2 id="13-v%E1%BA%A5n-%C4%91%E1%BB%81-2-blocking-l%C3%A0-%E2%80%9Cth%E1%BA%A3m-h%E1%BB%8Da%E2%80%9D">1.3. Vấn đề 2: Blocking là “thảm họa”</h2><p>Java sinh ra để viết code đồng bộ (synchronous), nên bạn viết thế này:</p><pre><code class="language-Java">String result = repository.fetchDataFromDB(query); // block</code></pre><p>Câu lệnh đơn giản, dễ hiểu. Nhưng lại block cả OS thread!</p><p>⇒ Nghĩa là: <strong>Trong thời gian chờ DB phản hồi, 1MB RAM + 1 OS thread hoàn toàn vô dụng</strong>.</p><p>Nếu bạn có hàng ngàn request đến một lúc, và mỗi cái block vài trăm mili giây, bạn sẽ sớm cạn tài nguyên.</p><figure class="kg-card kg-image-card"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe_CEZVivSNwv1-bqU5qc3bE-askq5UfBS5oRuSW8Dv-g_lP1apbI_X2CFmb7FhTlB1CJPk6KPx1JIP-tFGin_H9siFev4GUCpmvEd2KXb4nfS8wjidIMy5Ja5axvYpVY2Vu4m0?key=AN7_aS17-3rGf2aHU6nwVA" class="kg-image" alt="" loading="lazy" width="602" height="465"></figure><h2 id="14-v%E1%BA%A5n-%C4%91%E1%BB%81-3-reactive-programming-kh%C3%B4ng-d%E1%BB%85-nu%E1%BB%91t">1.4. Vấn đề 3: Reactive programming không dễ nuốt</h2><p>Giải pháp để tránh blocking là gì?</p><p>Chuyển sang dùng các mô hình bất đồng bộ như:</p><ul><li>Callback Hell (<code>CompletableFuture, ListenableFuture</code>)</li><li>Reactive Stack: Spring WebFlux, Reactor, RxJava...</li></ul><p><em>Các giải pháp trước Virtual Thread: Callback Hell và WebFlux</em> </p><p>Khi các lập trình viên Java phải đối mặt với giới hạn của Platform Thread, họ buộc phải tìm cách né tránh blocking I/O bằng những kỹ thuật bất đồng bộ. Hai trong số đó là: </p><h3 id="141-callback-hell-%E2%80%93-%C4%91%E1%BB%8Ba-ng%E1%BB%A5c-l%E1%BB%93ng-nhau">1.4.1. Callback Hell – "Địa ngục lồng nhau" </h3><p>Callback là kỹ thuật phổ biến để xử lý bất đồng bộ: bạn truyền một hàm (callback) để thực thi khi một tác vụ kết thúc. Giả sử bạn cần thực hiện 3 tác vụ bất đồng bộ theo chuỗi: gọi API, đọc file, rồi ghi vào DB. Với callback, bạn sẽ viết như sau: </p><pre><code class="language-Java">callApi(url, response -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;readFile(response.getFilePath(), content -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;saveToDb(content, result -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("All done!"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}); &nbsp;&nbsp;&nbsp;&nbsp;}); });</code></pre><ul><li>&nbsp;Nghe có vẻ ổn, nhưng:<ul><li>Code lồng nhau như bánh chưng nhiều lớp.</li><li><strong>Khó đọc, khó maintain, và khó test</strong>.</li><li>Debug dòng nào chạy trước, lỗi ở đâu — rất mệt mỏi.</li></ul></li></ul><p>Đây chính là thứ mà dân lập trình gọi là Callback Hell. </p><h3 id="142-spring-webflux-%E2%80%93-reactive-programming">1.4.2. Spring WebFlux – Reactive Programming </h3><p>Spring WebFlux ra đời để giải quyết bài toán này bằng mô hình Reactive, sử dụng Mono và Flux thay cho callback lồng nhau. Mục tiêu là: không block thread, dùng ít thread để phục vụ hàng chục ngàn request. Ví dụ khi viết một controller với WebFlux: <code>@GetMapping("/users/{id}")</code></p><pre><code class="language-Java">public Mono&lt;User&gt; getUser(@PathVariable String id) { &nbsp;&nbsp;&nbsp;&nbsp;return userService.findById(id) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.flatMap(user -&gt; enrichUser(user)) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;.flatMap(enrichedUser -&gt; validateUser(enrichedUser)); }</code></pre><p>Mọi thứ trông có vẻ đẹp hơn, không còn lồng callback. Nhưng vấn đề là:</p><ul><li>Mặc dù code <em>gần giống</em> code tuần tự, nhưng thật ra nó là flow bất đồng bộ. Log không chạy theo thứ tự như bạn nghĩ.</li><li>Khi cần trace lỗi, bạn sẽ gặp những dòng stacktrace dài dằng dặc đến từ Reactor Core.</li><li>Dùng Thread.currentThread().getName() để log thread hiện tại sẽ luôn thấy "reactor-http-nio-xxx", nên khó biết request nào đang làm gì.</li><li><strong>Debug trong IDE khó</strong>: không đặt được breakpoint như mong muốn, hoặc không biết nó chạy ở đâu, khi nào.</li></ul><p>Nếu bạn đang dùng WebFlux hoặc callback để tránh block thì Virtual Thread như một hơi thở mới: vẫn code tuần tự, vẫn gọi API, vẫn sleep, nhưng không sợ “đốt” tài nguyên thread như trước nữa.</p><p>Nhiều người ví von: <em>"Bạn không học reactive, bạn học cách sinh tồn trong reactor."</em></p><h2 id="15-%C4%91%C3%A3-%C4%91%E1%BA%BFn-l%C3%BAc-c%E1%BA%A7n-m%E1%BB%99t-l%E1%BB%91i-tho%C3%A1t">1.5. Đã đến lúc cần một lối thoát</h2><p>Lập trình viên Java cần một cách:</p><ul><li>Viết code <em>đồng bộ, tuyến tính</em> như cũ.</li><li>Nhưng chạy <em>hiệu quả, không blocking</em> như async.</li></ul><p>Đó chính là lúc Virtual Thread xuất hiện – <em>lightweight threads that reduce the effort of writing, maintaining, and debugging high-throughput concurrent applications</em>**.**</p><h1 id="2-virtual-thread-l%C3%A0-g%C3%AC-v%C3%A0-t%E1%BA%A1i-sao-n%C3%B3-thay-%C4%91%E1%BB%95i-cu%E1%BB%99c-ch%C6%A1i">2. Virtual thread là gì và tại sao nó thay đổi cuộc chơi?</h1><p>Được thử nghiệm từ Java 19 và release chính thức trong phiên bản Java 21 vào tháng 9 năm 2023, Virtual thread được Oracle giới thiệu là 1 kiểu thread lightweight, được thiết kế để giảm chi phí tài nguyên và tăng khả năng mở rộng cho các ứng dụng xử lý concurrent.</p><blockquote>💡 “<em>Virtual threads are lightweight threads that reduce the effort of writing, maintaining, and debugging high-throughput concurrent applications.</em>”</blockquote><p>Sau khi đã lăn lộn với platform thread và đủ mọi chiêu trò để scale ứng dụng – từ thread pool, async callback, reactive programming... cuối cùng Java mang đến một món quà: Virtual Thread.</p><p>Nhìn vào kiến trúc dưới đây, bạn sẽ thấy sự khác biệt:</p><figure class="kg-card kg-image-card"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXef2zOiuTN3wuR5NkGbT_gYX_Zt0BALS4NkUafL32-AM9mJbknkC8D_TIEbO4cJAtDF7EmVpIxqmvrylThTrGWDFyM1fUEB3HtDC5cadaZcjtJ3wymO8KpPbxtgKKjGB4Y0H2YW?key=AN7_aS17-3rGf2aHU6nwVA" class="kg-image" alt="" loading="lazy" width="1021" height="883"></figure><p>So với, mô hình platform thread, sự khác biệt lớn nhất là: Virtual thread không gắn chặt với OS thread. Thay vào đó, JVM có một lớp trung gian gọi là Carrier Thread – những thread thật sự từ OS, nhưng dùng để xử lý các virtual thread khi chúng cần chạy. <strong>Khi một virtual thread block (ví dụ chờ I/O, sleep, …), nó sẽ <em>"unmount"</em>, nhường lại carrier cho luồng khác.</strong></p><h2 id="21-v%E1%BA%ADy-chuy%E1%BB%87n-g%C3%AC-%C4%91ang-di%E1%BB%85n-ra">2.1. Vậy chuyện gì đang diễn ra?</h2><p>Khi bạn gọi Thread.start() (với thread được tạo bằng API virtual thread), JVM không nhảy ngay vào kernel để tạo thread thật như trước đây. Thay vào đó, chuỗi sự kiện sau diễn ra:</p><h3 id="211-t%E1%BA%A1o-virtual-thread-%E2%80%93-m%E1%BB%99t-object-nh%E1%BA%B9-n%E1%BA%B1m-trong-heap">2.1.1. Tạo Virtual Thread – Một object nhẹ nằm trong heap</h3><p>Virtual Thread đơn giản là một object trong Java heap – không ánh xạ trực tiếp đến native thread. Nó chứa các thông tin về logic thực thi (runnable), trạng thái và đặc biệt là một Continuation – đối tượng đại diện cho luồng thực thi tạm hoãn.</p><p>Lần đầu tiên bạn tạo Virtual Thread, JVM sẽ khởi tạo một ForkJoinPool đặc biệt, gọi là VirtualThreadScheduler, chứa một số Carrier Threads – chính là các Platform Thread phục vụ việc chạy các virtual thread.</p><p>💡 Mặc định, số lượng carrier thread bằng số core CPU.</p><h3 id="212-scheduling-kh%C3%B4ng-ch%E1%BA%A1y-ngay-%E2%80%93-x%E1%BA%BFp-h%C3%A0ng-%C4%91%C3%A3">2.1.2. Scheduling: Không chạy ngay – xếp hàng đã</h3><p>Virtual Thread được thêm vào một queue đợi trong scheduler. Nếu có một carrier thread rảnh, JVM sẽ "gắn" (mount) virtual thread này lên carrier thread đó để chạy.</p><h3 id="213-mounting-virtual-thread-ch%E1%BA%A1y-and-carrier-thread">2.1.3. Mounting: Virtual thread chạy and carrier thread</h3><p>Khi virtual thread được mount, nghĩa là đoạn mã bạn truyền vào Runnable sẽ được chạy trên stack của carrier thread – giống như một thread bình thường.</p><p>Tuy nhiên, điểm đặc biệt là JVM có thể tạm dừng và phục hồi việc thực thi virtual thread bất kỳ lúc nào, nhờ vào continuation – cơ chế ghi lại “điểm dừng” để resume lại sau.</p><h3 id="214-blocking-virtual-thread-b%E1%BB%8B-unmount">2.1.4. Blocking: Virtual thread bị unmount</h3><p>Khi virtual thread thực hiện các thao tác blocking như:</p><ul><li>Thread.sleep()</li><li>Gọi I/O blocking (như <code>InputStream.read()</code>)</li><li>Chờ Lock, Semaphore,...</li></ul><p>JVM sẽ:</p><ul><li>Ngắt việc chạy virtual thread.</li><li>Gỡ nó khỏi carrier thread (unmount).</li><li>Lưu lại trạng thái thực thi (program counter, stack frame, ...) vào continuation nằm trong heap.</li></ul><p>Carrier thread lúc này không bị block mà quay trở lại chạy virtual thread khác đang chờ.</p><h3 id="215-khi-ready-mount-l%E1%BA%A1i-v%C3%A0-ti%E1%BA%BFp-t%E1%BB%A5c-ch%E1%BA%A1y">2.1.5. Khi ready, mount lại và tiếp tục chạy</h3><p>Khi blocking operation hoàn tất (ví dụ I/O trả kết quả), virtual thread sẽ được scheduler gắn trở lại vào một carrier thread và tiếp tục thực thi từ điểm đã dừng, như chưa có gì xảy ra.</p><p>Và đặc biệt hơn: virtual thread không cần bạn thay đổi code theo reactive hay async style rối rắm. Bạn vẫn viết code theo phong cách blocking truyền thống, nhưng JVM sẽ lo phần tối ưu.</p><h2 id="22-nh%E1%BB%AFng-c%C3%A1i-wow-nh%E1%BA%ADn-%C4%91%C6%B0%E1%BB%A3c">2.2. Những cái Wow nhận được</h2><p>Sau khi đi qua cách virtual thread hoạt động – từ lúc được tạo, mount vào carrier thread, đến lúc unmount khi blocking – có lẽ bạn đã phần nào hình dung được cơ chế đằng sau sự “nhẹ nhàng” của nó. Nhưng điều khiến virtual thread trở nên thực sự đáng giá lại nằm ở những lợi ích rất rõ ràng mà nó mang lại cho lập trình viên Java.</p><p>Không phải là một cải tiến nửa vời. Đây là những “wow” đủ khiến ta phải nhìn lại cách mình viết code song song trước giờ:</p><ul><li><strong>Không chiếm OS resource cố định</strong>. Virtual thread không ánh xạ 1:1 với native thread. Vì thế, hệ điều hành không phải quản lý hàng triệu thread – điều mà trước đây là bất khả thi. Thread có thể chờ đợi (blocking) một cách tự nhiên – gọi sleep(), readLine(), lock() – nhưng nhờ vào cơ chế unmount, nó không chiếm CPU thật. Trong khi đó, carrier thread có thể chạy tiếp các luồng khác.</li><li><strong>Tạo/Huỷ dễ dàng</strong>: Một trong những lợi thế lớn nhất của virtual thread là chi phí tạo và huỷ cực thấp. Trong khi platform thread (OS thread) yêu cầu hệ điều hành cấp phát tài nguyên (như stack size ~1MB mỗi thread), virtual thread chỉ là một đối tượng nhẹ trong heap (chỉ vài KB cho mỗi thread). Và khi sử dụng xong, việc huỷ nó cũng đơn giản như để GC làm phần việc còn lại.</li><li><strong>Không cần context switch nặng nề dưới kernel</strong>. JVM tự xử lý việc chuyển trạng thái trong user-space, thay vì nhờ tới kernel như platform thread. Việc này nhẹ hơn rất nhiều: không cần phải lưu/khôi phục các thanh ghi CPU, stack pointer hay cache của kernel.</li><li><strong>Có thể scale tới hàng triệu thread</strong>. Mỗi virtual thread chỉ là một object nhẹ trong heap – không có stack riêng 1MB như OS thread, không yêu cầu kernel cấp phát tài nguyên ngay từ khi sinh ra.</li><li><strong>Gỡ bỏ nỗi ám ảnh đa luồng</strong>: Trước khi virtual thread ra đời, Java trong nhiều năm qua đã hình thành nên nhiều kỹ thuật nhằm tránh sử dụng quá nhiều thread, chẳng hạn như asynchronous callback, non-blocking I/O, reactive programming,… Những kỹ thuật này tuy hiệu quả về mặt tài nguyên, nhưng rất khó đọc, debug, và maintain. Với virtual thread, chúng ta có thể trở lại với cách viết code truyền thống: đồng bộ, tuần tự, dễ hiểu, nhưng vẫn đạt được khả năng xử lý hàng nghìn, thậm chí hàng triệu tác vụ song song. Virtual thread thực chất chỉ là một cách triển khai mới của java.lang.Thread và vẫn tuân theo các quy tắc đã có từ Java SE 1.0. Điều đó có nghĩa là lập trình viên không cần học thêm bất kỳ khái niệm mới nào để bắt đầu sử dụng virtual thread, bạn vẫn làm việc với Thread, Runnable, synchronized, wait/notify,... như trước đây.</li></ul><p>Tất cả những điều này mở ra khả năng mới cho các hệ thống server Java – nơi mà trước đây việc tạo nhiều thread thường đi kèm với những nỗi lo rất… “thời cổ điển”.</p><p>Lý thuyết thì kha khá rồi đấy, nhưng mà nói mồm thôi thì ai tin đúng không, nên mình sẽ chạy một đoạn code demo để so sánh sự khác biệt giữa platform thread và virtual thread nhé</p><h3 id="221-k%E1%BB%8Bch-b%E1%BA%A3n-t%E1%BA%A1o-100000-thread-v%C3%A0-th%E1%BB%B1c-hi%E1%BB%87n-call-http-request-t%C3%A1c-v%E1%BB%A5-io">2.2.1. Kịch bản: Tạo 100.000 thread và thực hiện call http request (tác vụ IO)</h3><p><em>Yêu cầu: Java 21 trở lên và bật --enable-preview nếu bạn dùng JDK 21.</em></p><pre><code class="language-Java">package com.example.virtualthread; import java.time.Duration; import java.util.concurrent.CountDownLatch; public class VirtualVsPlatformThreadWithRealIO { private static final int THREAD_COUNT = 100_000; public static void main(String[] args) throws InterruptedException { // System.out.println("--- Platform Threads Demo ---"); // runWithThreads(false); System.out.println("\\n--- Virtual Threads Demo ---"); runWithThreads(true); } private static void runWithThreads(boolean useVirtualThread) throws InterruptedException { Thread[] threads = new Thread[THREAD_COUNT]; CountDownLatch readyLatch = new CountDownLatch(THREAD_COUNT); CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch doneLatch = new CountDownLatch(THREAD_COUNT); Runnable task = () -&gt; { try { readyLatch.countDown(); startLatch.await(); // Đợi tín hiệu bắt đầu đồng loạt performHttpRequest(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { doneLatch.countDown(); } }; for (int i = 0; i &lt; THREAD_COUNT; i++) { threads[i] = useVirtualThread ? Thread.ofVirtual().unstarted(task) : new Thread(task); } for (Thread thread : threads) { thread.start(); } readyLatch.await(); // Chờ đến khi tất cả thread đều đã sẵn sàng long start = System.currentTimeMillis(); startLatch.countDown(); // Bắt đầu đồng loạt doneLatch.await(); // Chờ đến khi tất cả task hoàn thành long end = System.currentTimeMillis(); System.out.println((useVirtualThread ? "Virtual" : "Platform") + " threads total time: " + (end - start) + " ms"); } private static void performHttpRequest() { try { System.out.println("Inside thread: " + Thread.currentThread()); Thread.sleep(Duration.ofSeconds(5)); } catch (InterruptedException e) { throw new RuntimeException(e); } } }</code></pre><p>Kết quả là:</p><ul><li>Trường hợp sử dụng platform thread, đoạn code đã bắn lỗi java.lang.OutOfMemoryError trước khi kịp chạy xong. Bởi vì bản chất platform thread là wrapper giữa JVM và native OS thread, mà OS thread thì… không hề nhẹ.</li></ul><figure class="kg-card kg-image-card"><img src="https://roninhub.com/content/images/2025/08/virtual-thread.webp" class="kg-image" alt="" loading="lazy" width="1857" height="231" srcset="https://roninhub.com/content/images/size/w600/2025/08/virtual-thread.webp 600w, https://roninhub.com/content/images/size/w1000/2025/08/virtual-thread.webp 1000w, https://roninhub.com/content/images/size/w1600/2025/08/virtual-thread.webp 1600w, https://roninhub.com/content/images/2025/08/virtual-thread.webp 1857w" sizes="(min-width: 720px) 720px"></figure><ul><li>Trường hợp sử dụng virtual thread, dù tạo đến 100.000 virtual thread, chương trình vẫn chạy xong chỉ sau khoảng 14 giây, và không hề gây áp lực lên bộ nhớ hay CPU. Lý do rất đơn giản:<ul><li>Virtual thread không giữ một native thread riêng, mà chỉ được mount với carrier thread khi thực thi.</li><li>Khi gặp Thread.sleep(), virtual thread được unmount, nhường chỗ cho luồng khác, và sau đó được mount trở lại khi cần.</li></ul></li></ul><figure class="kg-card kg-image-card"><img src="https://roninhub.com/content/images/2025/08/virtual-thread_compare.webp" class="kg-image" alt="" loading="lazy" width="1804" height="243" srcset="https://roninhub.com/content/images/size/w600/2025/08/virtual-thread_compare.webp 600w, https://roninhub.com/content/images/size/w1000/2025/08/virtual-thread_compare.webp 1000w, https://roninhub.com/content/images/size/w1600/2025/08/virtual-thread_compare.webp 1600w, https://roninhub.com/content/images/2025/08/virtual-thread_compare.webp 1804w" sizes="(min-width: 720px) 720px"></figure><p>Và nếu bạn để ý, như mình đã đề cập số native thread mặc định bằng số core mà máy có nên ở đây, mình đang có 4 worker tương ứng với 4 core của máy.</p><h2 id="23-li%E1%BB%87u-virtual-thread-c%C3%B3-th%E1%BB%83-thay-th%E1%BA%BF-ho%C3%A0n-to%C3%A0n-platform-thread">2.3. Liệu Virtual Thread có thể thay thế hoàn toàn Platform Thread?</h2><p>Sau khi chứng kiến những ưu điểm vượt trội mà virtual thread mang lại trong các tác vụ I/O – đơn giản hóa code, tiết kiệm tài nguyên, scale thoải mái mà không lo OOM – nhiều người sẽ đặt ra câu hỏi: Liệu virtual thread có thể thay thế được platform thread trong mọi trường hợp?</p><p>Câu trả lời là: <strong>không hẳn</strong>.</p><p>Virtual thread được thiết kế để <strong>tối ưu cho các tác vụ blocking I/O</strong> – nơi luồng có thể tạm ngừng và nhường lại tài nguyên cho luồng khác. Nhưng trong thế giới <strong>CPU-bound, nơi các tác vụ phải sử dụng liên tục để tính toán, thì lợi thế này gần như biến mất</strong>.</p><p>Lúc này, cả virtual thread và platform thread đều cần phải trực tiếp cạnh tranh thời gian CPU, và vì JVM vẫn cần OS thread để thực thi các tác vụ tính toán, việc tạo ra hàng ngàn virtual thread có thể <strong>khiến hệ thống scheduler bị quá tải</strong>, dẫn tới hiệu năng không cải thiện – thậm chí còn tệ hơn.</p><p>Hãy cùng nhìn vào một ví dụ so sánh giữa hai loại thread khi thực hiện cùng một khối lượng công việc tính toán.</p><pre><code class="language-Java">package com.example.virtualthread; import java.util.concurrent.CountDownLatch; public class VirtualVsPlatformThreadCPU { &nbsp;&nbsp;&nbsp;&nbsp;private static final int THREAD_COUNT = 2000; // thử với 100, rồi nâng lên 500, 1000 &nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) throws InterruptedException { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("\\n--- Virtual Threads Demo ---"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;runWithThreads(true); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println("--- Platform Threads Demo ---"); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;runWithThreads(false); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;private static void runWithThreads(boolean useVirtualThread) throws InterruptedException { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread[] threads = new Thread[THREAD_COUNT]; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CountDownLatch readyLatch = new CountDownLatch(THREAD_COUNT); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CountDownLatch startLatch = new CountDownLatch(1); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CountDownLatch doneLatch = new CountDownLatch(THREAD_COUNT); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Runnable task = () -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;readyLatch.countDown(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;startLatch.await(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;performCpuIntensiveTask(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} catch (InterruptedException e) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread.currentThread().interrupt(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} finally { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;doneLatch.countDown(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 0; i &lt; THREAD_COUNT; i++) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;threads[i] = useVirtualThread &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;? Thread.ofVirtual().unstarted(task) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: new Thread(task); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (Thread thread : threads) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;thread.start(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;readyLatch.await(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;long start = System.currentTimeMillis(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;startLatch.countDown(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;doneLatch.await(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;long end = System.currentTimeMillis(); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println((useVirtualThread ? "Virtual" : "Platform") + &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" threads total time: " + (end - start) + " ms"); &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;private static void performCpuIntensiveTask() { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;long count = 0; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 2; i &lt; 100_000; i++) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (isPrime(i)) count++; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}; &nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;private static boolean isPrime(int n) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (n &lt;= 1) return false; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 2; i * i &lt;= n; i++) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (n % i == 0) return false; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return true; &nbsp;&nbsp;&nbsp;&nbsp;} }</code></pre><p>Trong đoạn code trên, ta sẽ lần lượt benmark với THREAD_COUNT = 100, 500, 2000 và dứới đây là bảng kết quả:</p> <!--kg-card-begin: html--> <table style="border:none;border-collapse:collapse;"><colgroup><col width="87"><col width="100"><col width="165"><col width="170"></colgroup><tbody><tr style="height:25pt"><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Case</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Số thread</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Platform thread (ms)</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;text-align: center;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Virtual thread (ms)</span></p></td></tr><tr style="height:25pt"><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">1</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">100</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">74</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">75</span></p></td></tr><tr style="height:25pt"><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">2</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">500</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">286</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">316</span></p></td></tr><tr style="height:25pt"><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">3</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">2000</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">864</span></p></td><td style="vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">922</span></p></td></tr></tbody></table> <!--kg-card-end: html--> <p>Có thể thấy dù virtual thread được thiết kế để “nhẹ” hơn platform thread, nhưng trong các tác vụ CPU-bound, sự khác biệt về hiệu năng giữa chúng là không đáng kể, thậm chí virtual thread còn có thể thua kém hơn khi chúng ta tăng số lượng thread lên.</p><p><strong>Nguyên nhân không nằm ở thread, mà là ở CPU.</strong></p><p>Mỗi tác vụ CPU-bound (như tính toán số nguyên tố) cần được CPU thực thi trực tiếp. Quay lại với sơ đồ hình 2.1, dù bạn tạo ra hàng triệu thread, thì nếu máy bạn chỉ có 4 core, tại một thời điểm, chỉ có 4 task được chạy thực sự. Thread chỉ là đơn vị quản lý luồng logic — để đạt được concurrency (tính đồng thời). Nhưng parallelism (tính song song) thì giới hạn bởi số lượng CPU core. Trong khi platform thread được quản lý bởi OS, OS scheduler có khả năng tối ưu việc phân phối thread đến CPU core, với ít overhead. Thì Virtual thread lại do JVM scheduler điều phối, nên phải thực hiện thêm các thao tác mounted/unmounted tới một carrier thread (OS thread). Việc mount/unmount này tạo thêm overhead trong môi trường có nhiều task tính toán liên tục và dẫn đến việc càng nhiều virtual thread được tạo, gánh nặng điều phối cũng sẽ tăng lên gây ảnh hưởng đến performance của hệ thống.</p><figure class="kg-card kg-image-card"><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeG4h26yqXv0AVINB4uOzYSQTPJe7A4SIVhArBImTYUIuU_wwHIOsIBbB3LyiWIWW4SQfjGDnYrdOUp5mq7k0Hidisenb310jBqo3cHdF7FeTBOEtSEEsy7--wrKA0RrRT549OGNQ?key=AN7_aS17-3rGf2aHU6nwVA" class="kg-image" alt="" loading="lazy" width="676" height="678"></figure><h1 id="3-best-practice">3. Best practice</h1><p><em>Virtual thread không phải là "silver bullet”</em></p><p>Virtual thread là một cuộc cách mạng cho lập trình đồng thời I/O-bound, nhưng không phải là phép màu cho mọi loại workload.</p><p>Nó mở ra một cách tiếp cận mới trong lập trình đồng thời: đơn giản hơn, dễ đọc hơn, và cực kỳ tiết kiệm tài nguyên. Tuy nhiên, virtual thread không giải quyết được mọi vấn đề, và đặc biệt không phải là giải pháp tối ưu trong mọi tình huống. Vậy thì khi nào nên dùng và không nên dùng virtual thread, cũng như dùng virtual thread như thế nào để đạt được kết quả tốt nhất?</p><p>Dưới đây là một số kinh nghiệm của mình để sử dụng virtual thread hiệu quả và để bạn không biến nó thành “con dao hai lưỡi”.</p><h2 id="31-io-bound-or-cpu-bound">3.1. IO-bound or CPU-bound</h2><p>Virtual thread thực sự tỏa sáng trong các ứng dụng IO-bound, nơi tác vụ chủ yếu là chờ phản hồi từ mạng, database, hoặc hệ thống file. Trong những trường hợp này, thread dành phần lớn thời gian để "ngồi chơi", và virtual thread giúp bạn có thể tạo hàng chục nghìn thread để xử lý song song mà không tốn nhiều tài nguyên.</p><p>Ngược lại, nếu ứng dụng của bạn chủ yếu là CPU-bound, nghĩa là nặng về xử lý tính toán thì virtual thread sẽ không mang lại lợi ích rõ ràng. Bottleneck lúc này không còn là số lượng thread, mà là số lõi CPU. Dù bạn có tạo hàng ngàn virtual thread, chúng vẫn phải tranh nhau từng chu kỳ CPU, dẫn đến:</p><ul><li>Context switch tăng mạnh, do nhiều thread cùng cạnh tranh chạy trên ít CPU.</li><li>Hiệu suất giảm, vì CPU mất thời gian để chuyển đổi giữa các task thay vì xử lý thực sự.</li><li>Tổng thể hệ thống có thể còn chậm hơn so với chỉ dùng vài platform thread xử lý tuần tự.</li></ul><h2 id="32-tr%C3%A1nh-d%C3%B9ng-thread-pool-ki%E1%BB%83u-c%C5%A9-fixedthreadpool-cachedthreadpool">3.2. Tránh dùng thread pool kiểu cũ (FixedThreadPool, CachedThreadPool)</h2><p>Trước khi virtual thread được release ở Java 21, thread hay platform thread là tài nguyên quý giá, nên chúng ta phải tái sử dụng bằng cách dùng các thread pool như Executors.newFixedThreadPool() hoặc newCachedThreadPool() để:</p><ul><li>Giới hạn số lượng thread đang chạy.</li><li>Giảm chi phí tạo và hủy thread.</li></ul><p>Nhưng với virtual thread, những giả định cũ không còn đúng nữa.</p><pre><code class="language-Java">ExecutorService pool = Executors.newFixedThreadPool(100); pool.submit(() -&gt; { &nbsp;&nbsp;&nbsp;&nbsp;// blocking I/O here });</code></pre><p>Đây là cách xử lý thường thấy khi chúng ta sử dụng platform thread, tạo một thread pool với số thread ban đầu là 100 và tái sử dụng chúng. Nhưng việc giới hạn 100 thread trong pool vô tình tạo ra bottleneck cho hệ thống. Khi 100 task đang blocking I/O, các task còn lại phải chờ, dù virtual thread có thể chạy song song hàng chục ngàn cái. Thay vì tái sử dụng thread, hãy tạo một virtual thread cho mỗi task bằng cách dùng:</p><pre><code class="language-Java">try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { &nbsp;&nbsp;&nbsp;&nbsp;Future&lt;ResultA&gt; f1 = executor.submit(task1); &nbsp;&nbsp;&nbsp;&nbsp;Future&lt;ResultB&gt; f2 = executor.submit(task2); &nbsp;&nbsp;&nbsp;&nbsp;// ... }</code></pre><p>Tại sao cách này tốt:</p><ul><li>Không có giới hạn cố định về số lượng thread.</li><li>Không cần quản lý pool.</li><li>Mỗi task có một thread riêng, không giành giật tài nguyên như trong pool truyền thống.</li><li>Executor này rất nhẹ, bạn có thể tạo mới cho từng request hoặc từng nhóm task mà không lo chi phí.</li></ul><h2 id="33-kh%C3%B4ng-tr%E1%BB%99n-l%E1%BA%ABn-virtual-thread-v%E1%BB%9Bi-async-style-code">3.3. Không trộn lẫn Virtual Thread với async-style code</h2><p>Virtual thread được sinh ra để đơn giản hóa lập trình đồng thời bằng cách cho phép bạn viết code như viết từng bước tuần tự, mà vẫn đạt được khả năng chạy song song và không bị block platform thread.</p><p>Nhưng nếu bạn tiếp tục sử dụng những kỹ thuật async truyền thống như:</p><ul><li>CompletableFuture.thenApply(...)</li><li>Reactive Stream (Mono, Flux, Observable…)</li><li>Callback hell (callback(callback(callback(...))))</li></ul><p>Thì bạn đang bỏ phí lợi ích lớn nhất của virtual thread,đó là trở lại với code đồng bộ dễ đọc, dễ debug, dễ maintain. Việc sử dụng async-style code với virtual thread là dư thừa, vì bản thân virtual thread đã xử lý vấn đề blocking cho bạn rồi.</p><h1 id="4-t%E1%BB%95ng-k%E1%BA%BFt">4. Tổng kết</h1><p>Virtual thread là một bước tiến thực sự đột phá trong của Java, nó mở ra khả năng xử lý hàng nghìn đến hàng triệu tác vụ đồng thời mà vẫn nhẹ nhàng, tiết kiệm tài nguyên.</p><p>Với virtual thread, bạn có thể viết code đồng bộ đơn giản theo kiểu truyền thống<strong> </strong>nhưng lại hiệu quả như asynchronous. Đặc biệt phù hợp cho các hệ thống:</p><ul><li>Web server high-concurrency.</li><li>Dịch vụ backend nhiều I/O.</li><li>Ứng dụng cần xử lý đồng thời mà không muốn vướng vào reactive hay callback hell.</li></ul><p>Tuy nhiên, như Fred Brooks từng nói: "There are no silver bullets in software engineering."</p><p>Virtual thread không phải là đạn bạc cho mọi vấn đề. Với các tác vụ CPU-bound, thread pool truyền thống vẫn có chỗ đứng riêng – bởi CPU không thể "ảo hóa" như thread.</p><p>Hy vọng bài viết trên đã giúp bạn hình dung rõ hơn về virtual thread, không chỉ là một tính năng mới, mà là một cách tiếp cận mới cho lập trình đồng thời trong Java: dễ viết, dễ hiểu, dễ scale.</p><p>Và đừng quên thực hành bằng cách chạy thử những dòng code trong bài nhé.</p><p>Many thanks and happy reading!</p><h1 id="5-references">5. References</h1><ul><li><a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html?ref=roninhub.com#GUID-E695A4C5-D335-4FA4-B886-FEB88C73F23E"><u>https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-E695A4C5-D335-4FA4-B886-FEB88C73F23E</u></a></li><li><a href="https://stackoverflow.com/questions/78318131/do-java-21-virtual-threads-address-the-main-reason-to-switch-to-reactive-single?ref=roninhub.com"><u>https://stackoverflow.com/questions/78318131/do-java-21-virtual-threads-address-the-main-reason-to-switch-to-reactive-single</u></a></li></ul><p></p><hr><p>✏️ System Design VN: <a href="https://fb.com/groups/systemdesign.vn%5C?ref=roninhub.com">https://fb.com/groups/systemdesign.vn</a><br>🎬 Youtube: <a href="https://youtube.com/@ronin-engineer?ref=roninhub.com">https://youtube.com/@ronin-engineer</a></p>

By @wuan.580

Bạn đã bao giờ viết một ứng dụng xử lý hàng ngàn request cùng lúc, và cảm thấy như mình đang chiến đấu với chính Java?

Bạn từng dùng ThreadPoolExecutor và vắt óc cân chỉnh số lượng thread cho "vừa đủ dùng", tránh thiếu nhưng cũng không dám dư vì sợ OutOfMemoryError?

Bạn từng nhăn mặt khi phải viết những dòng code callback chằng chịt, chỉ để tránh block một luồng? Và rồi đau đầu gỡ bug vì stacktrace rối như tơ vò?

Nếu câu trả lời là "có", thì bạn không đơn độc. Và bạn cũng sắp có một giải pháp: Virtual Thread – một trong những bước nhảy vọt quan trọng nhất của Java trong thập kỷ qua.

Ra mắt như một phần của Project Loom, virtual thread mở ra một cách tiếp cận hoàn toàn mới: viết code đồng bộ như bình thường, nhưng hiệu năng gần như bất đồng bộ. Không còn callback hell, không cần reactive framework phức tạp, và đặc biệt là, bạn có thể tạo hàng triệu luồng mà JVM vẫn mỉm cười nhẹ nhàng.

Trong bài viết này, chúng ta sẽ khám phá:

  • Virtual thread là gì và tại sao nó thay đổi cuộc chơi?
  • Cách JVM "ảo thuật" để luồng block không còn tốn kém.
  • Những khác biệt then chốt giữa virtual thread và platform thread.
  • Khi nào nên dùng virtual thread, và những tình huống cần cân nhắc.
  • Một số ví dụ thực tế: từ lý thuyết đến performance thực chiến.
Virtual thread không chỉ là một cải tiến về kỹ thuật, nó là lời tuyên bố: Java vẫn chưa già, và vẫn có thể hiện đại hóa một cách mạnh mẽ.

1. Thời tiền sử trước Virtual Thread: Platform Thread và giải pháp nửa vời

Trước khi Virtual Thread ra đời, Java đã có một hành trình dài loay hoay với bài toán xử lý đồng thời. Những gì chúng ta gọi là “platform thread” thực chất là một lớp bọc (wrapper) giữa JVM và OS thread – thứ sinh ra không phải để tạo ra hàng triệu luồng.

Trong phần này, chúng ta sẽ cùng nhìn lại các vấn đề đặc trưng của Platform Thread, và các giải pháp trước đây đã cố gắng khắc phục nó: thread pool, callback, reactive programming, ...

1.1. Platform Thread – Cách mà JVM làm việc với hệ điều hành

Trước khi bước vào thế giới của Virtual Thread, hãy dành chút thời gian để nhìn lại cách Java truyền thống xử lý luồng – thứ mà chúng ta gọi là Platform Thread.

Hình minh họa trên cho thấy cấu trúc hoạt động của Platform Thread qua 3 tầng:

  • JVM Layer: Mỗi Thread bạn tạo ra trong Java là một Platform Thread. JVM sẽ tạo một stack riêng cho từng thread và điều phối nó.
  • OS Layer: JVM ánh xạ 1:1 các Platform Thread với các native OS thread (dùng pthread trên Linux hoặc CreateThread trên Windows).
  • CPU Layer: Hệ điều hành sẽ lên lịch (schedule) các OS thread này chạy trên các CPU core thực sự.

Tại sao lại gọi là "Platform" Thread?

Bởi vì mỗi java.lang.Thread sẽ được nền tảng hệ điều hành (platform) cung cấp tài nguyên — bạn tạo một thread trong Java, và JVM phải “nhờ” OS cấp cho bạn một thread thực sự.

Điều này kéo theo một vài hệ quả sau:

1.2. Vấn đề 1: Thread quá "nặng"

Mỗi thread trong Java (platform thread) là một OS thread, điều đó có nghĩa là:

  • Tạo thread tốn chi phí vì JVM phải gọi native API để khởi tạo một OS thread.
  • Nếu bạn tạo 100.000 threads, bạn đang yêu cầu JVM sử dụng... 100GB RAM chỉ để giữ stack, nghe thôi đã thấy đốt tiền rồi :>

Mỗi thread chiếm ~ 1MB bộ nhớ stack.

⇒ Và rõ ràng điều này không khả thi. Vì thế lập trình viên buộc phải dùng thread pool, và bắt đầu bước vào mê cung của các ExecutorService, RejectedExecutionHandler, ThreadFactory, ...

  • Việc tạo thread tốn chi phí: Cần hệ điều hành cấp phát stack memory, context switching, register state, kernel resource...
  • Quản lý thread số lượng lớn không hiệu quả: Khi bạn muốn xử lý 1 triệu request đồng thời? Không thể nào tạo 1 triệu OS thread!
  • Bài toán blocking càng nghiêm trọng hơn: Nếu thread đó bị block do I/O (gọi HTTP, đọc DB...), OS thread đó vẫn bị chiếm dụng, lãng phí tài nguyên.

1.3. Vấn đề 2: Blocking là “thảm họa”

Java sinh ra để viết code đồng bộ (synchronous), nên bạn viết thế này:

String result = repository.fetchDataFromDB(query); // block

Câu lệnh đơn giản, dễ hiểu. Nhưng lại block cả OS thread!

⇒ Nghĩa là: Trong thời gian chờ DB phản hồi, 1MB RAM + 1 OS thread hoàn toàn vô dụng.

Nếu bạn có hàng ngàn request đến một lúc, và mỗi cái block vài trăm mili giây, bạn sẽ sớm cạn tài nguyên.

1.4. Vấn đề 3: Reactive programming không dễ nuốt

Giải pháp để tránh blocking là gì?

Chuyển sang dùng các mô hình bất đồng bộ như:

  • Callback Hell (CompletableFuture, ListenableFuture)
  • Reactive Stack: Spring WebFlux, Reactor, RxJava...

Các giải pháp trước Virtual Thread: Callback Hell và WebFlux

Khi các lập trình viên Java phải đối mặt với giới hạn của Platform Thread, họ buộc phải tìm cách né tránh blocking I/O bằng những kỹ thuật bất đồng bộ. Hai trong số đó là:

1.4.1. Callback Hell – "Địa ngục lồng nhau"

Callback là kỹ thuật phổ biến để xử lý bất đồng bộ: bạn truyền một hàm (callback) để thực thi khi một tác vụ kết thúc. Giả sử bạn cần thực hiện 3 tác vụ bất đồng bộ theo chuỗi: gọi API, đọc file, rồi ghi vào DB. Với callback, bạn sẽ viết như sau:

callApi(url, response -> {
    readFile(response.getFilePath(), content -> {
        saveToDb(content, result -> {
            System.out.println("All done!");
        });
    });
});
  •  Nghe có vẻ ổn, nhưng:
    • Code lồng nhau như bánh chưng nhiều lớp.
    • Khó đọc, khó maintain, và khó test.
    • Debug dòng nào chạy trước, lỗi ở đâu — rất mệt mỏi.

Đây chính là thứ mà dân lập trình gọi là Callback Hell.

1.4.2. Spring WebFlux – Reactive Programming

Spring WebFlux ra đời để giải quyết bài toán này bằng mô hình Reactive, sử dụng Mono và Flux thay cho callback lồng nhau. Mục tiêu là: không block thread, dùng ít thread để phục vụ hàng chục ngàn request. Ví dụ khi viết một controller với WebFlux: @GetMapping("/users/{id}")

public Mono<User> getUser(@PathVariable String id) {
    return userService.findById(id)
            .flatMap(user -> enrichUser(user))
            .flatMap(enrichedUser -> validateUser(enrichedUser));
}

Mọi thứ trông có vẻ đẹp hơn, không còn lồng callback. Nhưng vấn đề là:

  • Mặc dù code gần giống code tuần tự, nhưng thật ra nó là flow bất đồng bộ. Log không chạy theo thứ tự như bạn nghĩ.
  • Khi cần trace lỗi, bạn sẽ gặp những dòng stacktrace dài dằng dặc đến từ Reactor Core.
  • Dùng Thread.currentThread().getName() để log thread hiện tại sẽ luôn thấy "reactor-http-nio-xxx", nên khó biết request nào đang làm gì.
  • Debug trong IDE khó: không đặt được breakpoint như mong muốn, hoặc không biết nó chạy ở đâu, khi nào.

Nếu bạn đang dùng WebFlux hoặc callback để tránh block thì Virtual Thread như một hơi thở mới: vẫn code tuần tự, vẫn gọi API, vẫn sleep, nhưng không sợ “đốt” tài nguyên thread như trước nữa.

Nhiều người ví von: "Bạn không học reactive, bạn học cách sinh tồn trong reactor."

1.5. Đã đến lúc cần một lối thoát

Lập trình viên Java cần một cách:

  • Viết code đồng bộ, tuyến tính như cũ.
  • Nhưng chạy hiệu quả, không blocking như async.

Đó chính là lúc Virtual Thread xuất hiện – lightweight threads that reduce the effort of writing, maintaining, and debugging high-throughput concurrent applications**.**

2. Virtual thread là gì và tại sao nó thay đổi cuộc chơi?

Được thử nghiệm từ Java 19 và release chính thức trong phiên bản Java 21 vào tháng 9 năm 2023, Virtual thread được Oracle giới thiệu là 1 kiểu thread lightweight, được thiết kế để giảm chi phí tài nguyên và tăng khả năng mở rộng cho các ứng dụng xử lý concurrent.

💡 “Virtual threads are lightweight threads that reduce the effort of writing, maintaining, and debugging high-throughput concurrent applications.”

Sau khi đã lăn lộn với platform thread và đủ mọi chiêu trò để scale ứng dụng – từ thread pool, async callback, reactive programming... cuối cùng Java mang đến một món quà: Virtual Thread.

Nhìn vào kiến trúc dưới đây, bạn sẽ thấy sự khác biệt:

So với, mô hình platform thread, sự khác biệt lớn nhất là: Virtual thread không gắn chặt với OS thread. Thay vào đó, JVM có một lớp trung gian gọi là Carrier Thread – những thread thật sự từ OS, nhưng dùng để xử lý các virtual thread khi chúng cần chạy. Khi một virtual thread block (ví dụ chờ I/O, sleep, …), nó sẽ "unmount", nhường lại carrier cho luồng khác.

2.1. Vậy chuyện gì đang diễn ra?

Khi bạn gọi Thread.start() (với thread được tạo bằng API virtual thread), JVM không nhảy ngay vào kernel để tạo thread thật như trước đây. Thay vào đó, chuỗi sự kiện sau diễn ra:

2.1.1. Tạo Virtual Thread – Một object nhẹ nằm trong heap

Virtual Thread đơn giản là một object trong Java heap – không ánh xạ trực tiếp đến native thread. Nó chứa các thông tin về logic thực thi (runnable), trạng thái và đặc biệt là một Continuation – đối tượng đại diện cho luồng thực thi tạm hoãn.

Lần đầu tiên bạn tạo Virtual Thread, JVM sẽ khởi tạo một ForkJoinPool đặc biệt, gọi là VirtualThreadScheduler, chứa một số Carrier Threads – chính là các Platform Thread phục vụ việc chạy các virtual thread.

💡 Mặc định, số lượng carrier thread bằng số core CPU.

2.1.2. Scheduling: Không chạy ngay – xếp hàng đã

Virtual Thread được thêm vào một queue đợi trong scheduler. Nếu có một carrier thread rảnh, JVM sẽ "gắn" (mount) virtual thread này lên carrier thread đó để chạy.

2.1.3. Mounting: Virtual thread chạy and carrier thread

Khi virtual thread được mount, nghĩa là đoạn mã bạn truyền vào Runnable sẽ được chạy trên stack của carrier thread – giống như một thread bình thường.

Tuy nhiên, điểm đặc biệt là JVM có thể tạm dừng và phục hồi việc thực thi virtual thread bất kỳ lúc nào, nhờ vào continuation – cơ chế ghi lại “điểm dừng” để resume lại sau.

2.1.4. Blocking: Virtual thread bị unmount

Khi virtual thread thực hiện các thao tác blocking như:

  • Thread.sleep()
  • Gọi I/O blocking (như InputStream.read())
  • Chờ Lock, Semaphore,...

JVM sẽ:

  • Ngắt việc chạy virtual thread.
  • Gỡ nó khỏi carrier thread (unmount).
  • Lưu lại trạng thái thực thi (program counter, stack frame, ...) vào continuation nằm trong heap.

Carrier thread lúc này không bị block mà quay trở lại chạy virtual thread khác đang chờ.

2.1.5. Khi ready, mount lại và tiếp tục chạy

Khi blocking operation hoàn tất (ví dụ I/O trả kết quả), virtual thread sẽ được scheduler gắn trở lại vào một carrier thread và tiếp tục thực thi từ điểm đã dừng, như chưa có gì xảy ra.

Và đặc biệt hơn: virtual thread không cần bạn thay đổi code theo reactive hay async style rối rắm. Bạn vẫn viết code theo phong cách blocking truyền thống, nhưng JVM sẽ lo phần tối ưu.

2.2. Những cái Wow nhận được

Sau khi đi qua cách virtual thread hoạt động – từ lúc được tạo, mount vào carrier thread, đến lúc unmount khi blocking – có lẽ bạn đã phần nào hình dung được cơ chế đằng sau sự “nhẹ nhàng” của nó. Nhưng điều khiến virtual thread trở nên thực sự đáng giá lại nằm ở những lợi ích rất rõ ràng mà nó mang lại cho lập trình viên Java.

Không phải là một cải tiến nửa vời. Đây là những “wow” đủ khiến ta phải nhìn lại cách mình viết code song song trước giờ:

  • Không chiếm OS resource cố định. Virtual thread không ánh xạ 1:1 với native thread. Vì thế, hệ điều hành không phải quản lý hàng triệu thread – điều mà trước đây là bất khả thi. Thread có thể chờ đợi (blocking) một cách tự nhiên – gọi sleep(), readLine(), lock() – nhưng nhờ vào cơ chế unmount, nó không chiếm CPU thật. Trong khi đó, carrier thread có thể chạy tiếp các luồng khác.
  • Tạo/Huỷ dễ dàng: Một trong những lợi thế lớn nhất của virtual thread là chi phí tạo và huỷ cực thấp. Trong khi platform thread (OS thread) yêu cầu hệ điều hành cấp phát tài nguyên (như stack size ~1MB mỗi thread), virtual thread chỉ là một đối tượng nhẹ trong heap (chỉ vài KB cho mỗi thread). Và khi sử dụng xong, việc huỷ nó cũng đơn giản như để GC làm phần việc còn lại.
  • Không cần context switch nặng nề dưới kernel. JVM tự xử lý việc chuyển trạng thái trong user-space, thay vì nhờ tới kernel như platform thread. Việc này nhẹ hơn rất nhiều: không cần phải lưu/khôi phục các thanh ghi CPU, stack pointer hay cache của kernel.
  • Có thể scale tới hàng triệu thread. Mỗi virtual thread chỉ là một object nhẹ trong heap – không có stack riêng 1MB như OS thread, không yêu cầu kernel cấp phát tài nguyên ngay từ khi sinh ra.
  • Gỡ bỏ nỗi ám ảnh đa luồng: Trước khi virtual thread ra đời, Java trong nhiều năm qua đã hình thành nên nhiều kỹ thuật nhằm tránh sử dụng quá nhiều thread, chẳng hạn như asynchronous callback, non-blocking I/O, reactive programming,… Những kỹ thuật này tuy hiệu quả về mặt tài nguyên, nhưng rất khó đọc, debug, và maintain. Với virtual thread, chúng ta có thể trở lại với cách viết code truyền thống: đồng bộ, tuần tự, dễ hiểu, nhưng vẫn đạt được khả năng xử lý hàng nghìn, thậm chí hàng triệu tác vụ song song. Virtual thread thực chất chỉ là một cách triển khai mới của java.lang.Thread và vẫn tuân theo các quy tắc đã có từ Java SE 1.0. Điều đó có nghĩa là lập trình viên không cần học thêm bất kỳ khái niệm mới nào để bắt đầu sử dụng virtual thread, bạn vẫn làm việc với Thread, Runnable, synchronized, wait/notify,... như trước đây.

Tất cả những điều này mở ra khả năng mới cho các hệ thống server Java – nơi mà trước đây việc tạo nhiều thread thường đi kèm với những nỗi lo rất… “thời cổ điển”.

Lý thuyết thì kha khá rồi đấy, nhưng mà nói mồm thôi thì ai tin đúng không, nên mình sẽ chạy một đoạn code demo để so sánh sự khác biệt giữa platform thread và virtual thread nhé

2.2.1. Kịch bản: Tạo 100.000 thread và thực hiện call http request (tác vụ IO)

Yêu cầu: Java 21 trở lên và bật --enable-preview nếu bạn dùng JDK 21.

package com.example.virtualthread;

import java.time.Duration;
import java.util.concurrent.CountDownLatch;

public class VirtualVsPlatformThreadWithRealIO {
   private static final int THREAD_COUNT = 100_000;

   public static void main(String[] args) throws InterruptedException {
//        System.out.println("--- Platform Threads Demo ---");
//        runWithThreads(false);
       System.out.println("\\n--- Virtual Threads Demo ---");
       runWithThreads(true);
   }
   private static void runWithThreads(boolean useVirtualThread) throws InterruptedException {
       Thread[] threads = new Thread[THREAD_COUNT];
       CountDownLatch readyLatch = new CountDownLatch(THREAD_COUNT);
       CountDownLatch startLatch = new CountDownLatch(1);
       CountDownLatch doneLatch = new CountDownLatch(THREAD_COUNT);
       Runnable task = () -> {
           try {
               readyLatch.countDown();
               startLatch.await(); // Đợi tín hiệu bắt đầu đồng loạt
               performHttpRequest();
           } catch (InterruptedException e) {
               Thread.currentThread().interrupt();
           } finally {
               doneLatch.countDown();
           }
       };
   	for (int i = 0; i < THREAD_COUNT; i++) {
           threads[i] = useVirtualThread
                   ? Thread.ofVirtual().unstarted(task)
                   : new Thread(task);
       }
   	for (Thread thread : threads) {
           thread.start();
       }
       readyLatch.await(); // Chờ đến khi tất cả thread đều đã sẵn sàng
       long start = System.currentTimeMillis();
       startLatch.countDown(); // Bắt đầu đồng loạt
       doneLatch.await(); // Chờ đến khi tất cả task hoàn thành
       long end = System.currentTimeMillis();
       System.out.println((useVirtualThread ? "Virtual" : "Platform") +
               " threads total time: " + (end - start) + " ms");
   }
   private static void performHttpRequest() {
       try {
           System.out.println("Inside thread: " + Thread.currentThread());
           Thread.sleep(Duration.ofSeconds(5));
       } catch (InterruptedException e) {
           throw new RuntimeException(e);
       }
   }
}

Kết quả là:

  • Trường hợp sử dụng platform thread, đoạn code đã bắn lỗi java.lang.OutOfMemoryError trước khi kịp chạy xong. Bởi vì bản chất platform thread là wrapper giữa JVM và native OS thread, mà OS thread thì… không hề nhẹ.
  • Trường hợp sử dụng virtual thread, dù tạo đến 100.000 virtual thread, chương trình vẫn chạy xong chỉ sau khoảng 14 giây, và không hề gây áp lực lên bộ nhớ hay CPU. Lý do rất đơn giản:
    • Virtual thread không giữ một native thread riêng, mà chỉ được mount với carrier thread khi thực thi.
    • Khi gặp Thread.sleep(), virtual thread được unmount, nhường chỗ cho luồng khác, và sau đó được mount trở lại khi cần.

Và nếu bạn để ý, như mình đã đề cập số native thread mặc định bằng số core mà máy có nên ở đây, mình đang có 4 worker tương ứng với 4 core của máy.

2.3. Liệu Virtual Thread có thể thay thế hoàn toàn Platform Thread?

Sau khi chứng kiến những ưu điểm vượt trội mà virtual thread mang lại trong các tác vụ I/O – đơn giản hóa code, tiết kiệm tài nguyên, scale thoải mái mà không lo OOM – nhiều người sẽ đặt ra câu hỏi: Liệu virtual thread có thể thay thế được platform thread trong mọi trường hợp?

Câu trả lời là: không hẳn.

Virtual thread được thiết kế để tối ưu cho các tác vụ blocking I/O – nơi luồng có thể tạm ngừng và nhường lại tài nguyên cho luồng khác. Nhưng trong thế giới CPU-bound, nơi các tác vụ phải sử dụng liên tục để tính toán, thì lợi thế này gần như biến mất.

Lúc này, cả virtual thread và platform thread đều cần phải trực tiếp cạnh tranh thời gian CPU, và vì JVM vẫn cần OS thread để thực thi các tác vụ tính toán, việc tạo ra hàng ngàn virtual thread có thể khiến hệ thống scheduler bị quá tải, dẫn tới hiệu năng không cải thiện – thậm chí còn tệ hơn.

Hãy cùng nhìn vào một ví dụ so sánh giữa hai loại thread khi thực hiện cùng một khối lượng công việc tính toán.

package com.example.virtualthread;

import java.util.concurrent.CountDownLatch;

public class VirtualVsPlatformThreadCPU {
    private static final int THREAD_COUNT = 2000; // thử với 100, rồi nâng lên 500, 1000

    public static void main(String[] args) throws InterruptedException {
        System.out.println("\\n--- Virtual Threads Demo ---");
        runWithThreads(true);
        System.out.println("--- Platform Threads Demo ---");
        runWithThreads(false);
    }

    private static void runWithThreads(boolean useVirtualThread) throws InterruptedException {
        Thread[] threads = new Thread[THREAD_COUNT];
        CountDownLatch readyLatch = new CountDownLatch(THREAD_COUNT);
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch doneLatch = new CountDownLatch(THREAD_COUNT);

        Runnable task = () -> {
            try {
                readyLatch.countDown();
                startLatch.await();
                performCpuIntensiveTask();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                doneLatch.countDown();
            }
        };

        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = useVirtualThread
                    ? Thread.ofVirtual().unstarted(task)
                    : new Thread(task);
        }

        for (Thread thread : threads) {
            thread.start();
        }

        readyLatch.await();
        long start = System.currentTimeMillis();
        startLatch.countDown();
        doneLatch.await();
        long end = System.currentTimeMillis();

        System.out.println((useVirtualThread ? "Virtual" : "Platform") +
                " threads total time: " + (end - start) + " ms");
    }

    private static void performCpuIntensiveTask() {
        long count = 0;
        for (int i = 2; i < 100_000; i++) {
            if (isPrime(i)) count++;
        };
    }

    private static boolean isPrime(int n) {
        if (n <= 1) return false;
        for (int i = 2; i * i <= n; i++) {
            if (n % i == 0) return false;
        }
        return true;
    }
}

Trong đoạn code trên, ta sẽ lần lượt benmark với THREAD_COUNT = 100, 500, 2000 và dứới đây là bảng kết quả:

Case

Số thread

Platform thread (ms)

Virtual thread (ms)

1

100

74

75

2

500

286

316

3

2000

864

922

Có thể thấy dù virtual thread được thiết kế để “nhẹ” hơn platform thread, nhưng trong các tác vụ CPU-bound, sự khác biệt về hiệu năng giữa chúng là không đáng kể, thậm chí virtual thread còn có thể thua kém hơn khi chúng ta tăng số lượng thread lên.

Nguyên nhân không nằm ở thread, mà là ở CPU.

Mỗi tác vụ CPU-bound (như tính toán số nguyên tố) cần được CPU thực thi trực tiếp. Quay lại với sơ đồ hình 2.1, dù bạn tạo ra hàng triệu thread, thì nếu máy bạn chỉ có 4 core, tại một thời điểm, chỉ có 4 task được chạy thực sự. Thread chỉ là đơn vị quản lý luồng logic — để đạt được concurrency (tính đồng thời). Nhưng parallelism (tính song song) thì giới hạn bởi số lượng CPU core. Trong khi platform thread được quản lý bởi OS, OS scheduler có khả năng tối ưu việc phân phối thread đến CPU core, với ít overhead. Thì Virtual thread lại do JVM scheduler điều phối, nên phải thực hiện thêm các thao tác mounted/unmounted tới một carrier thread (OS thread). Việc mount/unmount này tạo thêm overhead trong môi trường có nhiều task tính toán liên tục và dẫn đến việc càng nhiều virtual thread được tạo, gánh nặng điều phối cũng sẽ tăng lên gây ảnh hưởng đến performance của hệ thống.

3. Best practice

Virtual thread không phải là "silver bullet”

Virtual thread là một cuộc cách mạng cho lập trình đồng thời I/O-bound, nhưng không phải là phép màu cho mọi loại workload.

Nó mở ra một cách tiếp cận mới trong lập trình đồng thời: đơn giản hơn, dễ đọc hơn, và cực kỳ tiết kiệm tài nguyên. Tuy nhiên, virtual thread không giải quyết được mọi vấn đề, và đặc biệt không phải là giải pháp tối ưu trong mọi tình huống. Vậy thì khi nào nên dùng và không nên dùng virtual thread, cũng như dùng virtual thread như thế nào để đạt được kết quả tốt nhất?

Dưới đây là một số kinh nghiệm của mình để sử dụng virtual thread hiệu quả và để bạn không biến nó thành “con dao hai lưỡi”.

3.1. IO-bound or CPU-bound

Virtual thread thực sự tỏa sáng trong các ứng dụng IO-bound, nơi tác vụ chủ yếu là chờ phản hồi từ mạng, database, hoặc hệ thống file. Trong những trường hợp này, thread dành phần lớn thời gian để "ngồi chơi", và virtual thread giúp bạn có thể tạo hàng chục nghìn thread để xử lý song song mà không tốn nhiều tài nguyên.

Ngược lại, nếu ứng dụng của bạn chủ yếu là CPU-bound, nghĩa là nặng về xử lý tính toán thì virtual thread sẽ không mang lại lợi ích rõ ràng. Bottleneck lúc này không còn là số lượng thread, mà là số lõi CPU. Dù bạn có tạo hàng ngàn virtual thread, chúng vẫn phải tranh nhau từng chu kỳ CPU, dẫn đến:

  • Context switch tăng mạnh, do nhiều thread cùng cạnh tranh chạy trên ít CPU.
  • Hiệu suất giảm, vì CPU mất thời gian để chuyển đổi giữa các task thay vì xử lý thực sự.
  • Tổng thể hệ thống có thể còn chậm hơn so với chỉ dùng vài platform thread xử lý tuần tự.

3.2. Tránh dùng thread pool kiểu cũ (FixedThreadPool, CachedThreadPool)

Trước khi virtual thread được release ở Java 21, thread hay platform thread là tài nguyên quý giá, nên chúng ta phải tái sử dụng bằng cách dùng các thread pool như Executors.newFixedThreadPool() hoặc newCachedThreadPool() để:

  • Giới hạn số lượng thread đang chạy.
  • Giảm chi phí tạo và hủy thread.

Nhưng với virtual thread, những giả định cũ không còn đúng nữa.

ExecutorService pool = Executors.newFixedThreadPool(100);
pool.submit(() -> {
    // blocking I/O here
});

Đây là cách xử lý thường thấy khi chúng ta sử dụng platform thread, tạo một thread pool với số thread ban đầu là 100 và tái sử dụng chúng. Nhưng việc giới hạn 100 thread trong pool vô tình tạo ra bottleneck cho hệ thống. Khi 100 task đang blocking I/O, các task còn lại phải chờ, dù virtual thread có thể chạy song song hàng chục ngàn cái. Thay vì tái sử dụng thread, hãy tạo một virtual thread cho mỗi task bằng cách dùng:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<ResultA> f1 = executor.submit(task1);
    Future<ResultB> f2 = executor.submit(task2);
    // ...
}

Tại sao cách này tốt:

  • Không có giới hạn cố định về số lượng thread.
  • Không cần quản lý pool.
  • Mỗi task có một thread riêng, không giành giật tài nguyên như trong pool truyền thống.
  • Executor này rất nhẹ, bạn có thể tạo mới cho từng request hoặc từng nhóm task mà không lo chi phí.

3.3. Không trộn lẫn Virtual Thread với async-style code

Virtual thread được sinh ra để đơn giản hóa lập trình đồng thời bằng cách cho phép bạn viết code như viết từng bước tuần tự, mà vẫn đạt được khả năng chạy song song và không bị block platform thread.

Nhưng nếu bạn tiếp tục sử dụng những kỹ thuật async truyền thống như:

  • CompletableFuture.thenApply(...)
  • Reactive Stream (Mono, Flux, Observable…)
  • Callback hell (callback(callback(callback(...))))

Thì bạn đang bỏ phí lợi ích lớn nhất của virtual thread,đó là trở lại với code đồng bộ dễ đọc, dễ debug, dễ maintain. Việc sử dụng async-style code với virtual thread là dư thừa, vì bản thân virtual thread đã xử lý vấn đề blocking cho bạn rồi.

4. Tổng kết

Virtual thread là một bước tiến thực sự đột phá trong của Java, nó mở ra khả năng xử lý hàng nghìn đến hàng triệu tác vụ đồng thời mà vẫn nhẹ nhàng, tiết kiệm tài nguyên.

Với virtual thread, bạn có thể viết code đồng bộ đơn giản theo kiểu truyền thống nhưng lại hiệu quả như asynchronous. Đặc biệt phù hợp cho các hệ thống:

  • Web server high-concurrency.
  • Dịch vụ backend nhiều I/O.
  • Ứng dụng cần xử lý đồng thời mà không muốn vướng vào reactive hay callback hell.

Tuy nhiên, như Fred Brooks từng nói: "There are no silver bullets in software engineering."

Virtual thread không phải là đạn bạc cho mọi vấn đề. Với các tác vụ CPU-bound, thread pool truyền thống vẫn có chỗ đứng riêng – bởi CPU không thể "ảo hóa" như thread.

Hy vọng bài viết trên đã giúp bạn hình dung rõ hơn về virtual thread, không chỉ là một tính năng mới, mà là một cách tiếp cận mới cho lập trình đồng thời trong Java: dễ viết, dễ hiểu, dễ scale.

Và đừng quên thực hành bằng cách chạy thử những dòng code trong bài nhé.

Many thanks and happy reading!

5. References

  • https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-E695A4C5-D335-4FA4-B886-FEB88C73F23E
  • https://stackoverflow.com/questions/78318131/do-java-21-virtual-threads-address-the-main-reason-to-switch-to-reactive-single


✏️ System Design VN: https://fb.com/groups/systemdesign.vn
🎬 Youtube: https://youtube.com/@ronin-engineer

java
middle
virtual_thread
concurrency

Bài viết liên quan

Java Concurrency (Phần 1): Thread

Một thread là một đơn vị thực thi nhỏ hơn một process. Một process có thể tạo ra nhiều thread trong quá trình thực thi. Tất cả các thread trong cùng một process sẽ chia sẻ, dùng chung một số vùng nhớ với nhau.

Unit Testing (Phần 1)

Unit Test giúp dự án phần mềm phát triển một cách bền vững.

Materialized view với PostgreSQL

Khái niệm view trong relational database chắc hẳn đã quá quen thuộc với anh em dev. View chỉ lưu trữ câu lệnh truy vấn, không lưu kết quả truy vấn và thực hiện truy vấn trên (các) bảng gốc mỗi khi view được truy cập. Vậy nếu truy vấn của view là một truy vấn phức tạp cho thống kế, cần JOIN nhiều bảng, nhiều điều kiện FILTER, tập dữ liệu ở bảng gốc lớn, có nhiều phép tổng hợp như SUM, AVG… Thì các bạn có đoán được vấn đề gì sẽ xuất hiện không? Nếu câu trả lời của các bạn là “hiệu suất của truy

DFS & BFS - Khi dữ liệu không được lưu trữ một cách tuyến tính

By Bùi Đức Toàn “Muốn tìm trân châu nơi đáy biển, ắt phải lặn sâu. Muốn nhìn toàn cảnh rừng xanh, phải leo lên đỉnh núi.” - Cổ nhân. Từ thuở khai hoang, con người khi thì đào sâu xuống lòng đất để tìm vàng, khi thì lại lần theo dòng sông để biết nó chảy về đâu. Trong thế giới khoa học máy tính, tinh thần khai hoang đó được tái hiện qua hai chiến lược kinh điển trong việc duyệt đồ thị là: 1. Tìm kiếm theo chiều sâu (Depth-First Search – DFS). 2. Tìm kiếm theo chiều rộng (Breadth-First Search

Stack & Queue - Khi array không chỉ để lưu trữ

Stack & Queue là hai cấu trúc dữ liệu cơ bản và phổ biến trong con đường của một lập trình viên, hẳn rằng những lập trình viên có kinh nghiệm đều đã nghe, đã biết đến cấu trúc dữ liệu này, vậy cuối cùng thì, Stack & Queue là gì và nó có tác dụng thế nào? QUEUE - HÀNG ĐỢI Để bắt đầu nhẹ nhàng, chúng ta bắt đầu với Queue trước. Trước tiên thì Queue là một cấu trúc dữ liệu có dạng FIFO (First In Frist Out) tức là vào trước thì ra trước. Một ví dụ dễ thấy trong cuộc sống hàng ngày là việc xếp

Tất cả bài viết
logo

HỘ KINH DOANH LẬP VƯƠNG

Giấy chứng nhận đăng ký doanh nghiệp số: 8656162915-001. Cấp ngày 21/02/2024. Nơi cấp: Sở Kế hoạch và Đầu tư TP. Hà Nội

PHƯƠNG THỨC THANH TOÁN

vnpay

LIÊN HỆ

roninengineer88@gmail.com

0362228388

26 ngõ 156 Hồng Mai, Hai Bà Trưng, Hà Nội

THEO DÕI CHÚNG TÔI

Facebook

Youtube

Tiktok

CHÍNH SÁCH

Chính sách bảo mật

Chính sách thanh toán

Đổi trả/Hoàn tiền

Hướng dẫn thanh toán VNPAY

PHƯƠNG THỨC THANH TOÁN

vnpay

Ronin Engineer 2024