Java Internals & Concurrency/Mô hình ánh xạ thread — user, kernel và platform thread
3/75
Bài 3 / 75~10 phútConcurrency cơ bảnMiễn phí lượt xem

Mô hình ánh xạ thread — user, kernel và platform thread

Ba mô hình ánh xạ user thread lên kernel thread (M:1, 1:1, M:N), vì sao OS hiện đại chọn one-to-one, và tại sao platform thread của Java không hề rẻ.

TL;DR: Thread được hỗ trợ ở hai cấp: user thread (quản lý ở user space bởi thư viện) và kernel thread (do hệ điều hành lập lịch trực tiếp). User thread ánh xạ xuống kernel thread theo ba mô hình: many-to-one (nhiều user thread dùng chung một kernel thread — rẻ nhưng không chạy song song và một blocking call chặn cả process), one-to-one (mỗi user thread một kernel thread — song song thật nhưng thread đắt), và many-to-many (multiplex, cân bằng hai phía). Java platform thread theo one-to-one nên mỗi thread là một OS thread — không hề rẻ; đó là lý do tồn tại thread pool và virtual thread (gần với many-to-many).

Bài 01 — Process và Thread đã trả lời thread là gì và vì sao nó nhẹ hơn process. Nhưng "nhẹ" tới đâu còn tuỳ một chi tiết chưa mở: một thread ở tầng ngôn ngữ thực sự ánh xạ xuống đâu trong hệ điều hành? Câu trả lời quyết định thread rẻ hay đắt, và chạy được song song hay không.

1. User thread và kernel thread

Thread được hỗ trợ ở hai cấp độ. User thread quản lý ở user space, thường bởi một thread library; kernel có thể không biết đến từng user thread. Kernel thread do hệ điều hành quản lý trực tiếp, scheduler của OS lập lịch thẳng cho chúng. Hầu hết hệ điều hành hiện đại — Windows, Linux, macOS — đều hỗ trợ kernel thread.

Câu hỏi quan trọng: user thread ánh xạ xuống kernel thread như thế nào? Ba mô hình phổ biến là many-to-one, one-to-one và many-to-many — chúng giải thích vì sao runtime như JVM phải cân bằng giữa "thread rẻ" và "thread chạy được song song".

2. Ba mô hình ánh xạ user thread lên kernel thread

Hàng trên trong sơ đồ là user thread (user space), hàng dưới là kernel thread (kernel space). Chú ý số kernel thread ở đáy mỗi mô hình: đó là số "ghế" thực sự mà OS scheduler nhìn thấy.

flowchart TB
  subgraph M1["Many-to-one (M:1)"]
    direction TB
    A1["User thread 1"] --> AK1["Kernel thread"]
    A2["User thread 2"] --> AK1
    A3["User thread 3"] --> AK1
  end
  subgraph O1["One-to-one (1:1)"]
    direction TB
    B1["User thread 1"] --> BK1["Kernel thread 1"]
    B2["User thread 2"] --> BK2["Kernel thread 2"]
    B3["User thread 3"] --> BK3["Kernel thread 3"]
  end
  subgraph MN["Many-to-many (M:N)"]
    direction TB
    C1["User thread 1"] --> CK1["Kernel thread 1"]
    C2["User thread 2"] --> CK1
    C3["User thread 3"] --> CK2["Kernel thread 2"]
    C4["User thread 4"] --> CK2
  end
  style AK1 fill:#FCD34D
  style BK1 fill:#6EE7B7
  style BK2 fill:#6EE7B7
  style BK3 fill:#6EE7B7
  style CK1 fill:#C4B5FD
  style CK2 fill:#C4B5FD
Thử đoán

Trong mô hình many-to-one, nhiều user thread chia sẻ đúng một kernel thread. Nếu một user thread gọi một blocking system call (ví dụ đọc file phải chờ đĩa), điều gì xảy ra với các user thread còn lại trong cùng process? Viết ra dự đoán của bạn trước khi đọc tiếp.

2.1 Many-to-one

Nhiều user-level thread ánh xạ vào một kernel thread duy nhất. Ưu điểm: quản lý thread rất nhanh, vì phần lớn thao tác nằm ở user space, không cần kernel can thiệp. Nhược điểm rất lớn: một thread gọi blocking system call thì cả process bị block — kernel chỉ thấy một kernel thread, không biết còn user thread khác muốn chạy; và vì chỉ có một kernel thread, các user thread không thể chạy song song trên nhiều core. Java đời đầu từng dùng mô hình tương tự qua Green Threads, nay ít dùng vì không khai thác được multicore.

2.2 One-to-one

Mỗi user thread ánh xạ tới một kernel thread. Concurrency tốt hơn many-to-one: một thread bị block vì system call thì các thread khác vẫn chạy, và nhiều thread chạy song song được trên multicore. Nhược điểm: mỗi user thread cần một kernel thread, nên tạo quá nhiều thread thì hệ thống chịu chi phí lớn về bộ nhớ, scheduling và context switching.

2.3 Many-to-many và two-level

Nhiều user-level thread được multiplex lên một số kernel thread nhỏ hơn hoặc bằng, kết hợp ưu điểm của hai mô hình trước: developer tạo nhiều user thread, runtime ánh xạ chúng xuống số kernel thread phù hợp. Một thread bị block thì kernel vẫn schedule kernel thread khác, mà hệ thống vẫn chạy song song được trên nhiều core.

Đổi lại, many-to-many khó triển khai hơn (một scheduler hai tầng); khi số core tăng và chi phí kernel thread thành chấp nhận được, hầu hết hệ điều hành hiện đại nghiêng về one-to-one. Một biến thể là mô hình two-level: user thread vẫn multiplex lên nhiều kernel thread, nhưng một user thread cụ thể có thể bind trực tiếp vào một kernel thread.

3. Vì sao platform thread của Java không hề rẻ?

JVM không "giả lập" việc lập lịch thread tách biệt khỏi hệ điều hành. Với platform thread truyền thống, Java thread ánh xạ xuống native thread của OS theo đúng mô hình one-to-one — tạo một platform thread tức là tạo hoặc dùng một kernel thread tương ứng.

Nhờ đó Java thread tận dụng được parallelism trên multicore, nhưng cũng vì thế thread không hề miễn phí. Tạo quá nhiều platform thread gây ra:

  • Tốn bộ nhớ cho stack của mỗi thread.
  • Tăng chi phí context switching.
  • Gây áp lực lên scheduler của hệ điều hành.
  • Làm ứng dụng khó kiểm soát khi số request tăng cao.

Tạo platform thread trực tiếp:

Thread worker = new Thread(() -> {
    System.out.println("Run in platform thread");
});
worker.start();

Đơn giản, nhưng mỗi request lại tạo một thread mới không kiểm soát thì hệ thống nhanh chóng cạn tài nguyên lúc tải cao. Có hai hướng giải quyết, mỗi hướng có bài riêng:

  • Thread pool / ExecutorService tách mô tả task khỏi quản lý thread: tái sử dụng một số cố định platform thread, nên kiểm soát được số lượng thread và giảm chi phí tạo/hủy. Chi tiết ở bài 17 — Executor và thread pool.
  • Virtual thread phá quan hệ one-to-one: JVM multiplex rất nhiều virtual thread lên một nhúm nhỏ platform thread (gọi là carrier thread), nên tạo hàng loạt thread cho tác vụ blocking I/O mà không trả chi phí của chừng đó OS thread — đưa Java về gần mô hình many-to-many. Chi tiết ở bài 20 — Virtual threads.

Điểm cần nhớ: trần chi phí của platform thread đến thẳng từ mô hình one-to-one — đó là động lực chung cho cả hai hướng trên.

4. Liên hệ các bài khác

  • Process và Thread — nền tảng: thread là gì, chia sẻ heap và giữ riêng stack; bài này xây tiếp trên đó để trả lời thread ánh xạ xuống OS ra sao.
  • Executor và thread pool — hướng thứ nhất để thuần hoá chi phí platform thread: tái dùng thread qua pool thay vì tạo mới mỗi task.
  • Virtual threads — hướng thứ hai: mô hình many-to-many nhắc ở mục 3 được khai triển đầy đủ, vì sao virtual thread cho phép hàng triệu Java thread trên một nhúm carrier thread.

5. 📚 Deep Dive

📚 Tài liệu chính thức (tuỳ chọn)

Spec / reference:

  • JEP 444 — Virtual Threads — đặc tả virtual thread hoàn thiện (final) trong Java 21: mô hình carrier thread, mount/unmount và động lực thiết kế.
  • Java SE 21 — java.lang.Thread — phân biệt platform thread vs virtual thread ngay trong Javadoc của lớp Thread.

Ghi chú: Đọc phần mở đầu JEP 444 để thấy vì sao mô hình one-to-one của platform thread đặt trần lên số kết nối đồng thời một server giữ được, và virtual thread gỡ trần đó ra sao.

6. Tóm tắt

  • Thread có hai cấp: user thread (user space) và kernel thread (do OS lập lịch). Cách chúng ánh xạ với nhau quyết định thread rẻ hay chạy được song song.
  • Ba mô hình: many-to-one (rẻ, nhưng một blocking call chặn cả process và không dùng được multicore), one-to-one (song song thật, thread đắt), many-to-many (multiplex, cân bằng — nhưng phức tạp).
  • Đa số OS hiện đại chọn one-to-one vì đơn giản và kernel thread đã đủ rẻ; many-to-many chủ yếu sống lại ở tầng runtime (virtual thread).
  • Java platform thread theo one-to-one nên mỗi thread là một OS thread — không rẻ. Thread pool tái dùng thread; virtual thread bỏ quan hệ one-to-one, multiplex lên carrier thread (gần many-to-many).

7. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao mô hình many-to-one (như Green Threads cũ) tạo/quản lý thread rất rẻ nhưng không được dùng phổ biến ngày nay?

Vì nhiều user thread ánh xạ vào một kernel thread duy nhất, phần lớn thao tác thread nằm ở user space nên rất nhanh. Nhưng hệ quả: (1) một blocking system call ở một thread làm block cả process — kernel chỉ thấy một kernel thread; (2) không thể chạy song song trên nhiều CPU core.

Khi phần cứng multicore trở nên phổ biến, không khai thác được nhiều core là nhược điểm chí mạng, nên mô hình này bị thay bằng one-to-one (và sau này là many-to-many cho virtual thread).

Q2
Về lý thuyết many-to-many cân bằng nhất (thread rẻ + chạy song song). Vì sao đa số OS hiện đại vẫn nghiêng về one-to-one?

Many-to-many phải multiplex user thread lên kernel thread ngay ở tầng runtime — một scheduler hai tầng, khó cài đặt đúng. Khi số core tăng và chi phí một kernel thread giảm xuống mức chấp nhận được, lợi ích "thread rẻ" của many-to-many không còn đủ bù cho độ phức tạp đó.

One-to-one đơn giản (để OS scheduler lo hết) và vẫn chạy song song tốt, nên trở thành lựa chọn mặc định của Linux, Windows, macOS. Many-to-many chủ yếu quay lại ở tầng runtime ngôn ngữ, ví dụ virtual thread của Java.

Q3
JVM tạo platform thread theo mô hình ánh xạ nào? Vì sao tạo 10.000 platform thread là ý tưởng tồi?

Platform thread theo one-to-one: mỗi Java platform thread là một kernel thread của OS. Vì vậy 10.000 platform thread nghĩa là 10.000 OS thread — tốn bộ nhớ cho 10.000 stack, tăng chi phí context switching, và gây áp lực lên scheduler của OS.

Chính trần chi phí này là động lực cho thread pool (tái dùng một số cố định thread) và virtual thread (bỏ quan hệ one-to-one để tạo thread gần như miễn phí cho tác vụ I/O).

Q4
Virtual thread gần nhất với mô hình ánh xạ nào? carrier thread đóng vai trò gì?

Virtual thread gần với many-to-many: JVM multiplex rất nhiều virtual thread lên một tập nhỏ hơn các platform thread gọi là carrier thread.

Khi một virtual thread cần chạy, nó được mount lên một carrier thread (được OS lập lịch như thread thường); khi gặp blocking I/O phù hợp, nó unmount, nhường carrier thread cho virtual thread khác. Nhờ vậy một nhúm carrier thread phục vụ được rất nhiều virtual thread đang chờ I/O.

Q5
Pool có 100 platform thread, cả 100 đang chờ database. Request thứ 101 phải chờ dù CPU còn rảnh. Vì sao virtual thread giải quyết được còn fixed thread pool thì không?

Fixed thread pool dựa trên platform thread (one-to-one với OS thread). 100 thread block I/O nghĩa là 100 OS thread bị giữ; thread thứ 101 không có chỗ chạy dù CPU rảnh, vì OS thread đắt nên không thể tạo vô hạn.

Virtual thread theo many-to-many: khi một virtual thread gặp blocking I/O phù hợp, nó unmount khỏi carrier thread, trả carrier thread đó cho virtual thread khác chạy. Nhờ vậy một số ít carrier thread phục vụ được rất nhiều virtual thread đang chờ I/O — mô hình "thread per request" trở nên khả thi. Lưu ý: chỉ đúng cho workload I/O-bound; với CPU-bound nặng, số core vẫn là trần thật.

Bài tiếp theo: Thread API và vòng đời — start, join, interrupt

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Thread API và vòng đời — start, join, sleep, daemon