1. Sự phát triển của Thread models
Trong mấy năm đi làm mình đã chứng kiến 4 model khi nhắc đến Multithreading, có thể tổng hợp lại như sau:
- Synchronous Blocking
- Asynchronous Blocking
- Asynchronous Partially Non Blocking
- Fully Non Blocking (Reactive model)
Reactive Programing giải quyết triệt để bài toán resource khi thực hiện các task yêu cầu IO trong multithreading.
Nhưng Reactive Programing có rất nhiều nhược điểm như code rất phức tạp và khó hiểu, khó trace log, khó debug, ...
Với Java 21, có một cách đơn giản hơn rất rất nhiều để đạt được 'gần như' là Non Blocking - mình đang nói về Virtual Thread
2. Virtual Thread
Virtual thread là một lightweight thread, về cơ bản nó cũng có các tính năng tương tự Platform Thread nhưng chi phí resource lại thấp hơn rất nhiều
Khi code chạy trên Virtual Thread thực hiện blocking I/O call, Virtual Thread được giữ lại để chờ response từ I/O nhưng Platform thread được trả lại về Thread Pool để handle task khác
Cho phép việc tạo và quản lý số lượng lớn concurrent tasks hiệu quả hơn, thể hiện rõ hơn là khi thực hiện blocking I/O call
3. Virtual Thread vs Platform Thread
Trước khi Virtual thread xuất hiện, Thread trong Java sử dụng là Platform Thread
Platform Thread được gắn cứng với OS Thread, nên khi Platform Thread bị block (thực hiện IO) đồng nghĩa với việc OS thread cũng bị block. Những điểm yếu có thể kể:
- Resource Heavy: Mỗi Platform thread sử dụng gần 1MB stack size
- Scalability Issues: Số lượng thread bị giới hạn bởi OS thread
- Complexity: Khó khăn trong việc xử lý code có khả năng scale
Virtual Thread cũng gắn trên OS Thread (thật ra là được JVM mount vào Platform Thread ) nhưng khác ở chổ khi thực hiện blocking call, Virtual Thread sẽ trả OS thread về lại pool, OS thread được giải phóng cho task khác
Vì vậy với những task bị blocking nhiều cho IO call, Virtual Thread sẽ giải quyết được bài toán về resource Nhưng với những task nặng về CPU bound, Platform Thread sẽ giải quyết tốt hơn (vì cơ chế context swtiching được xử lý bởi CPU thay vì JVM như Virtual Thread)
Virtual Thread hoàn toàn không nhanh hơn Platform Thread, nó chỉ giúp hệ thống tiết kiệm tài nguyên hơn từ đó tăng throughput bởi khả năng scale
4. Virtual Thread lại tiết kiệm resource nhiều cỡ nào?
Như hình ở trên có thể thấy chỉ với 1 OS Thread (bọc bởi Carrier Thread), có thể thực hiện được rất nhiều task execute bởi Virtual Thread
Lấy ví dụ cụ thể hơn mình có 1000 OS thread trong pool, khi hệ thống chạy 1000 concurrent task có thực hiện blocking IO call
Với Platform Thread:
- Mỗi 1 Platform thread sẽ giữ 1 OS thread để thực hiện task
- Khi gặp blocking call, 1000 OS thread sẽ đứng chờ cho tới khi nhận response
- Lúc này nếu có task 1001 submit vào sẽ phải chờ vì không còn thread nào free
- Sau khi hoàn thành task OS thread mới được release
Với Virtual Thread:
- Mỗi 1 Virtual Thread cũng sẽ lấy 1 OS thread để thực hiện task
- Khi gặp blocking call, OS thread sẽ được trả lại về pool, chỉ có Virtual thread đứng chờ response (suspend)
- Lúc này nếu có task 1001 submit vào vẫn sẽ có OS thread để execute (thậm chí có thể submit thêm 1000 task khác ) )
- Khi Virtual Thread nhận được response nó sẽ lại lấy 1 OS thread khác và xử lý tiếp cho đến khi xong task
5. Demo
2 cách init Virtual Thread, nhìn chung khá giống với cách init thread truyền thống
Thread.startVirtualThread(() -> { // Do some work System.out.println("Running in virtual thread: " + Thread.currentThread());
});
ExecutorService myExecutor=Executors.newVirtualThreadPerTaskExecutor());
Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
future.get();
System.out.println("Task completed");
Execute 1 triệu Virtual thread xem được ko nào @@
public static void main(String[] args) throws InterruptedException { Instant start = Instant.now(); Set<Long> vThreadIds = new HashSet<>(); var vThreads = IntStream.range(0, 1_000_000) .mapToObj(i -> Thread.ofVirtual().unstarted(() -> { vThreadIds.add(Thread.currentThread().getId()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } })).toList(); vThreads.forEach(Thread::start); for (var thread : vThreads) { thread.join(); } Instant end = Instant.now(); System.out.println("Time =" + Duration.between(start, end).toMillis() + " ms"); System.out.println("Number of unique vThreads used " + vThreadIds.size()); } Output:
Time = 4482 ms
Number of unique vThreads used 1000000
Work perfectly nhé !!!
Output:
Time = 4482 ms
Number of unique vThreads used 1000000
6. Lưu ý với Virtual Thread
Như đã nói ở trên Virtual Thread chỉ thích hợp cho các task có thực hiện blocking call
Java không thể nhận biết được code nằm trong synchronize block có bị block hay không, nên việc dùng synchronize (cho cả function or block) đều làm giảm hiệu suất (pinned). Có thể giải quyết bằng cách sử dụng Lock.
Cần lưu ý khi thực hiện file hoặc network call, JVM có thể handle 1 triệu request với Virtual Thread nhưng có đảm bảo là OS cho phép thực hiện 1 triệu network call?
Bonus: Virtual Thread được implement như thế nào?
Với Virtual Thread, cơ chế Context Switching được handle bởi JVM thay vì OS.
JVM quản lý hàng ngàn Virtual Thread bằng cách multiplexing vào một số lượng nhỏ cách Platform Thread. Tận dụng tối đa OS thread (Platform Thread wrap trên OS thread)
Khi 1 Virtual Thread chạy, JVM mount nó vào 1 Platform Thread., lúc này sẽ được gọi là Carrier Thread
Sau khi execute code xong hoặc khi thực hiện 1 blocking call, JVM sẽ unmount Virtual Thread khỏi Carrier Thread đó
Sau khi unmount, Carrier thread đó free và có thể dùng cho Virtual Thread khác