Virtual threads trong Java 21 là gì ?
Xin chào, xin chào ace đã quay lại với mình, nếu là lần đầu hoặc đã là lần thứ hai thì ace đều có thể theo giõi mình qua các bài viết tại đây
Hoặc có thể theo dõi Blog của mình tại dohv.stackgrowth.asia hoặc kênh Youtube của mình tại đây
Rất mong được sự ủng hộ, đón đọc đón xem của ace và đó cũng là một niềm động lực to lớn để mình có thể ra nhiều bài viết nhiều video hơn để phục vụ mọi người và để đóng góp làm tài liệu cho cộng đồng.
Quay trở lại với bài viết
Một lần nữa anh chị em cũng có thể theo dõi bài viết về Virtual threads trên blog cá nhận của mình. Thanks ace 👋👋👋👋
Sau bao nhiều ngày Virtual Thread bản preview từ Java 19 thì Java 21 đã chính thức được Released và một framework lớn của Java như Spring Boot và đồng loạt các Database như MongoDB... đã hỗ trợ VirtualThread
Thì Nhân dịp cuối năm 2023 âm lịch, đi làm ở trên công ty cũng khá rảnh và được anh giao cho nhiệm vụ đó là tìm hiểu thêm về Java 21 và đặc biệt là tính năng mới virtual thread thì hôm nay mình sẽ chia sẻ với anh tài liệu tham khảo để có thể thêm hiểu biết của mình về Java.
Trong bài viết để dẫn dắt vào bài viết và để anh em đọc dễ, hiểu sâu hơn về Java VirtualThread thì mình có tham khảo và trích dẫn một vài nội dung trong bài viết của tác giả dat bui .
Let's go.
Gì thì gì cứ phải là phải có yêu cầu đặt ra hay vấn đề cần giải quyết thì mới có phiên bản mới và nâng cấp đúng không anh em và mình giới thiệu một vấn đề team mình gặp phải ngay trong dự án đó là hết Thread => request rơi vào trạng thái blocking
Bài toán tận dụng tối đa multi-processors
Virtual Thread nó ra đời nhằm mục đích gì nó có liên quan gì tới việc tận dụng multi-processors.
Bài toán đặt ra ta có 5000 tasks cần 5000 Threads thực thi song song đồng thời?
Tất nhiên là nó không ổn vì các lý do dưới đây.
- Server chỉ có số lượng Processor từ 8, 16, 64
- Với JVM OS 64-bit để tạo 1 Thread cần ít nhất là 1024 bytes ~ 1MB (con số tham khảo từ Internet). Càng nhiều Thread thì cần càng nhiều tài nguyên.
- Context switch do Scheduler có thể lưu lại trạng thái của Thread, khôi phục trạng thái và thực thi nên việc chuyển đổi bộ nhớ register, chuyển đổi vị trí con trỏ, cập nhật các thông tin liên quan… và chi phí để thực thi là rất rất lớn
Cách giải quyết thông thường là dùng Thread Pool một tập hợp n threads thực thi m tasks. Cụ thể với Java có ExecutorService sẽ đảm nhận nhiệm vụ này.
Tuy nhiên nó nảy sinh ra bài toán khác, điều chỉnh size trong pool như thế nào cho phù hợp. Nếu size quá lớn sẽ giống bài toán 1000 threads ở trên, nhưng size quá nhỏ thì không tận dụng được multi-processors, hoặc latency cao vì phải xử lý nhiều task.
⇒ Các task chưa được thực thi rơi vào trạng thái blocking.
Multi-tasking trong xử lý đa luồng
Cơ chế giao Thread cho Processor xử lý
- Scheduler
- Scheduling
Scheduling cơ chế để giao Thread cho các Processor
Có 2 loại scheduling cho Scheduler là:
- Preemtive scheduling / Preemtive multitasking
- Cooperative scheduling / Non-preemtive multitasking
Cooperative scheduling
Các Thread tự quản lý vòng đời của chúng. Scheduler chỉ có nhiệm vụ điều phối các xử lý Thread cho Processor
**Preemptive scheduling **
Scheduler sẽ toàn quyền kiểm soát việc giao Thread cho Processor, từ việc Thread nào được thực thi cho đến thời gian thực thi bao lâu.
Việc ưu và nhược điểm của 2 cơ chế này mình sẽ có bài viết sâu hơn nhé, hehe
=> Thread/OS Thread là đơn vị cơ bản để CPU thực thi thông qua cơ chế scheduling của Scheduler
Tìm hiểu kỹ hơn về Threading model
CPU thread – Hardware thread: luồng xử lý của processor. Thông thường mỗi processor xử lý một luồng, với những CPU hỗ trợ hyper-threading có thể xử lý được hai luồng đồng thời với nhau.
OS – Thread: các luồng thực thi code được quản lý bởi OS và điều phối bởi Scheduler. Với Java từ 21 trở xuống thì 1 Thread được mapping 1 – 1 với OS Thread
Kernel-Level Threading
Chính là OS Thread là đơn vị cơ bản nhất được quản lý/thực thi bởi Scheduler và OS.
Lấy ví dụ là ngôn ngữ Java để giải thích:
Java hỗ trợ việc tạo Thread và thông qua System call và mapping Java Thread với Thread của OS.
Việc mapping 1 – 1 này Java Thread sử dụng chung Scheduler với OS nên sẽ có nhược điểm:
Càng nhiều Thread thì Context-Switch càng xảy ra thường xuyên ảnh hưởng tới Performance của hệ thống.
User-Level Threading
Còn được gọi là Green Thread hay là Lightweight Thread. OS và Scheduler do đó không hề biết tới sự tồn tại của Lightweight Thread.
(đây chính là thứ chúng ta cần quan tâm nhiều hơn)
Với User-level Threading model, các user-thread (Lightweight Thread) được mapping N – 1 với OS thread. Ta cần một bộ Scheduler riêng để quản lý việc này trong quá trình runtime.
Việc quyết định cơ chế Preemtive scheduling hay Cooperative scheduling lúc này do các developer/engineer, không còn phụ thuộc vào OS. Context switch xảy ra ở application, dễ dàng kiểm soát hơn, cost dành cho nó cũng giảm đi nhiều so với OS thread và process.
Hybrid threading
Tận dụng ưu điểm của cả 2 mô hình trên và kết nối chúng lại với nhau.
Các kernel-level thread vẫn có thể thực thi song song trên môi trường Multi-processors, đồng thời các user-level thread được sử dụng để thực thi đồng thời với nhau.
Như vậy, Context switch được chuyển một phần từ low-level lên high-level, dễ dàng kiểm soát, quản lý hơn, từ đó tăng performance cho hệ thống.
Từ Java 21 trở xuống Java core đang áp dụng mô hình đầu tiên: Kernel-Threading Model để thực hiện Multi-thread programming.
Từ Java 21 Java core đã triển khai dựa trên Hybrid threading model
===> Tới đây thì ta quay lại với câu hỏi lúc đầu Virtual Thread sinh ra có liên quan gì tới tận Multi-processor.
Giới thiệu về ForkJoinPool
Tương tự như Java ExecutorService. Nhưng với một sự khác biệt. Nó phân chia các tác vụ cho các luồng thực thi trong Thread Pool. Framework Fork/ Join sử dụng thuật toán Work-stealing. Các luồng sẽ thực thi công việc của mình trên một bộ xử lý riêng biệt (thread/ processor), khi làm hết việc của mình, nó lấy bớt (steal) các tác vụ từ các luồng khác đang bận rộn.
Work stealing
Word Stealing là cơ chế giúp scheduler (có thể là trên ngôn ngữ, hoặc OS) có thể thực hiện việc tạo thên M thread mới hoạt động mượt mà trên N core, với M có thể lớn hơn N rất nhiều.
Idea của work-stealing scheduler là mỗi một core sẽ có một queue những task phải làm. Mỗi task đó bao gồm một list các instructions phải thực hiện một cách tuần tự. Khi một processor làm hết việc của mình, nó sẽ nhìn ngó sang các processor xung quanh, xem có gì cần làm không và “steal” công việc từ đó
Hoạt động của ForkJoinPool
Gồm 2 bước được thực hiện đệ quy. bước chia tách (fork/ split) và bước gộp (join/ merge).
Nguyên tắc hoạt động của Fork
Một nhiệm vụ (parent task) sử dụng nguyên tắc fork và join có thể chia tách (fork/ split) chính nó vào các nhiệm vụ con (sub task) nhỏ hơn để có thể được thực hiện đồng thời
Bằng cách chia nhỏ thành các nhiệm vụ con, mỗi nhiệm vụ con có thể được thực hiện song song bởi các CPU khác nhau, hoặc các luồng khác nhau trên cùng một CPU.
Nguyên tắc hoạt động của Join
Khi một nhiệm vụ (parent task) đã tự tách mình thành các nhiệm vụ con (sub task), nhiệm vụ cha sẽ đợi cho đến khi các nhiệm vụ con hoàn thành.
Khi nhiệm vụ con đã hoàn thành, nhiệm vụ cha có thể kết hợp (join/ merge) tất cả các kết quả con vào một kết quả cuối cùng.
Tạo một ForkJoinPool với tham số là số lượng luồng hoặc các CPU muốn làm việc đồng thời trên các nhiệm vụ được truyền vào ForkJoinPool. Nếu không xác định numOfProcessor, nó sẽ lấy số bộ vi xử lý có sẵn cho máy ảo Java để thực thi
=> Nó có liên quan gì mà được nhắc tới thì sẽ được giải thích ở phía dưới.
Virtual threads Java 21
Điểm qua một vài thông tin chính
A Virtual thread is an instance of java.lang.Thread
Không chia sẻ tài nguyên giữa các VirtualThread
Là một Lightweiht Thread được quản lý trên tầng Application.
Virtual thread được lưu ở bộ nhớ Heap của Java garbage-collected, Chi phí để tạo chỉ khoảng vài trăm bytes, mỗi VirtualThread là một Deep Call Stack
=> Do vậy nó không cần phải dùng tới Pool để quản lý
Scheduling Virtual Threads Java 21
Như đã nhắc tới ở Thread Modeling thì các Lightweight Thread sẽ cần được Schedule
Với JDK 21 nguyên văn tài liệu như sau:
The JDK’s scheduler assigns virtual threads to platform threads (this is the M:N scheduling of virtual threads mentioned earlier). The platform threads are then scheduled by the OS as usual
Có thể hiểu là Scheduler sẽ làm việc mapping M:N giữa Virtual Threads với Platform Threads và được thực thi bởi Scheduler của OS .
Lưu ý: khi 1 VirtualThread bị block bởi I/O operation
Ví dụ như: BlockingQueue.take()… Thì VirtualThread đó sẽ bị Un-Mapping với Platform Thread (hay OS Thread) cho tới khi nó được tiếp tục. Platform Thread đó không bị block và vẫn được mapping với các VirtualThread khác.
Scheduler của VirtualThread là 1 Work-Stealing ForkJoinPool được nhắc tới ở bên trên.
⇒ Tính song song và tận dụng tối đa được Processor là số Platform Thread của ForkJoinPool mặc định bằng số Processor của VM
=> Virtual Thread Scheduler quản lý một số Platform thread ít hơn so với số Virtual thread. Giảm Content-Switch tối ưu hóa việc sử dụng tài nguyên hệ thống tăng hiệu năng xử lý.