Java Internals & Concurrency/Process và Thread — từ nền tảng hệ điều hành đến Java Concurrency
1/28
Bài 1 / 28~20 phútConcurrency cơ bảnMiễn phí lượt xem

Process và Thread — từ nền tảng hệ điều hành đến Java Concurrency

Process vs program, bố cục bộ nhớ của process, vì sao có thread và thread chia sẻ/giữ riêng gì. User thread vs kernel thread và 3 mô hình ánh xạ (M:1, 1:1, M:N). Java thread: platform thread one-to-one, ExecutorService, và virtual thread (many-to-many).

1. Câu chuyện lịch sử

Ở những máy tính đời đầu, mọi thứ đơn giản hơn rất nhiều.

Máy tính khi đó chưa có hệ điều hành theo nghĩa hiện đại. Tại một thời điểm, hệ thống thường chỉ cho phép một chương trình được thực thi từ đầu đến cuối. Chương trình đó gần như nắm toàn quyền kiểm soát máy: nó có thể sử dụng CPU, truy cập bộ nhớ, thao tác với thiết bị nhập/xuất và làm việc trực tiếp với tài nguyên phần cứng mà không phải chia sẻ với chương trình nào khác.

Cách vận hành này nghe có vẻ đơn giản, nhưng nó kéo theo hai vấn đề lớn.

Thứ nhất, việc viết chương trình chạy trực tiếp trên phần cứng trần — bare metal — rất khó. Lập trình viên phải quan tâm đến quá nhiều chi tiết cấp thấp của phần cứng, thay vì tập trung vào logic chính của chương trình.

Thứ hai, chỉ cho phép một chương trình chạy tại một thời điểm là cách sử dụng tài nguyên rất kém hiệu quả. Máy tính thời kỳ đầu vốn đắt đỏ và khan hiếm. Nếu một chương trình đang chờ thao tác nhập/xuất, chẳng hạn như đọc dữ liệu từ thiết bị ngoại vi, CPU có thể bị bỏ không trong khi đáng lẽ nó có thể phục vụ một chương trình khác.

Các hệ thống máy tính hiện đại thì khác.

Nhiều chương trình có thể cùng được nạp vào bộ nhớ và cùng tồn tại trong quá trình thực thi. Bạn có thể vừa mở trình duyệt, vừa soạn tài liệu, vừa nghe nhạc, trong khi phía sau vẫn có nhiều dịch vụ hệ thống âm thầm hoạt động.

Khi nhiều chương trình cùng chạy, hệ điều hành không thể để mỗi chương trình tự do sử dụng tài nguyên như trước. Nó cần kiểm soát chặt chẽ hơn: chương trình nào được dùng CPU, chương trình nào được cấp bộ nhớ, chương trình nào được truy cập thiết bị I/O, và quan trọng hơn, chương trình này không được tùy tiện ảnh hưởng đến chương trình khác.

Từ nhu cầu quản lý, cô lập và điều phối đó, khái niệm process ra đời.

2. Process: chương trình khi bước vào trạng thái thực thi

2.1 Vì sao hệ điều hành cần nhiều process?

Có ba động lực quan trọng dẫn đến sự phát triển của các hệ điều hành cho phép nhiều chương trình cùng thực thi: tận dụng tài nguyên, tính công bằng và sự tiện lợi.

Tận dụng tài nguyên

Chương trình đôi khi phải chờ các thao tác bên ngoài, chẳng hạn như nhập dữ liệu, xuất dữ liệu, đọc file, ghi file hoặc chờ phản hồi từ thiết bị I/O. Trong thời gian chờ, chương trình không thể làm công việc hữu ích nào.

Thay vì để CPU rảnh rỗi, hệ điều hành có thể chuyển CPU sang phục vụ process khác. Cách làm này giúp tận dụng tài nguyên hệ thống tốt hơn.

Có thể hình dung đơn giản: nếu một chương trình đang chờ dữ liệu từ ổ đĩa, CPU không nhất thiết phải đứng yên cùng chương trình đó. Hệ điều hành có thể tạm dừng process đang chờ, chuyển CPU sang process khác, rồi quay lại khi dữ liệu đã sẵn sàng.

Tính công bằng

Nhiều người dùng và nhiều chương trình có thể cùng có nhu cầu sử dụng tài nguyên của máy. Nếu để một chương trình chạy đến khi hoàn tất rồi mới chuyển sang chương trình khác, các chương trình còn lại có thể phải chờ quá lâu.

Hệ điều hành giải quyết vấn đề này bằng cách chia nhỏ thời gian sử dụng CPU thành các lát thời gian — time slices — và phân phối chúng cho nhiều process.

Nhờ đó, nhiều chương trình có cảm giác như đang chạy đồng thời, dù tại một thời điểm cụ thể, một CPU core chỉ đang thực thi một dòng lệnh nhất định.

Sự tiện lợi

Trong nhiều trường hợp, việc viết nhiều chương trình nhỏ, mỗi chương trình đảm nhiệm một nhiệm vụ riêng, rồi cho chúng phối hợp với nhau khi cần, sẽ dễ hiểu và dễ bảo trì hơn so với viết một chương trình khổng lồ xử lý mọi thứ.

Đây cũng là một trong những tư tưởng quan trọng trong thiết kế hệ thống: chia nhỏ trách nhiệm, cô lập phần việc và để các thành phần phối hợp thông qua giao diện rõ ràng.

2.2 Process là gì?

Một cách phi hình thức, process là một chương trình đang trong trạng thái thực thi.

Nói cách khác, nếu program là mã chương trình còn nằm yên trên đĩa, thì process là phiên bản đang chạy của chương trình đó trong hệ thống. Process là đơn vị công việc cơ bản mà hệ điều hành dùng để quản lý các hoạt động đang diễn ra.

Một process không chỉ bao gồm mã lệnh. Nó còn có trạng thái thực thi, tài nguyên được cấp phát và ngữ cảnh riêng.

Trạng thái hiện tại của một process thường được biểu diễn thông qua:

  • Program counter — cho biết instruction tiếp theo sẽ được thực thi.
  • Processor registers — chứa các giá trị trung gian và trạng thái xử lý của CPU.
  • Không gian địa chỉ bộ nhớ — nơi lưu mã lệnh, dữ liệu, heap và stack.
  • Các tài nguyên hệ điều hành — chẳng hạn như file đang mở, quyền bảo mật, socket, signal, v.v.

Hệ điều hành cấp phát cho mỗi process một tập tài nguyên nhất định, chẳng hạn như bộ nhớ, file handles và security credentials. Các process được cô lập với nhau, nghĩa là một process thông thường không thể tự ý truy cập trực tiếp vào vùng nhớ hoặc tài nguyên nội bộ của process khác.

Sự cô lập này là một trong những lý do khiến hệ điều hành hiện đại có thể chạy nhiều chương trình cùng lúc mà vẫn giữ được tính ổn định. Nếu một chương trình gặp lỗi, lý tưởng nhất là lỗi đó chỉ làm process tương ứng bị ảnh hưởng, thay vì kéo sập toàn bộ hệ thống hoặc phá hỏng dữ liệu của process khác.

Khi cần giao tiếp, các process phải sử dụng các cơ chế giao tiếp liên tiến trình — inter-process communication — do hệ điều hành cung cấp, ví dụ như socket, signal handler, shared memory, semaphore hoặc file.

Một điểm rất quan trọng cần phân biệt: program không phải là process.

Program là một thực thể thụ động — passive entity. Nó có thể là một file chứa danh sách instruction được lưu trên đĩa, thường gọi là executable file.

Process là một thực thể chủ động — active entity. Nó có program counter, trạng thái thực thi riêng, không gian bộ nhớ riêng và tập tài nguyên được hệ điều hành cấp phát.

Một program trở thành process khi executable file được nạp vào bộ nhớ và bắt đầu thực thi. Việc này có thể xảy ra khi người dùng nhấp đúp vào biểu tượng ứng dụng, hoặc khi người dùng nhập lệnh chạy chương trình trên command line.

2.3 Bố cục bộ nhớ của một process

Bộ nhớ của một process thường được chia thành các vùng chính:

  • Text section: chứa mã thực thi — executable code.
  • Data section: chứa các biến toàn cục — global variables.
  • Heap section: chứa vùng nhớ được cấp phát động trong thời gian chạy.
  • Stack section: chứa dữ liệu tạm thời khi gọi hàm, chẳng hạn như tham số hàm, địa chỉ trả về và biến cục bộ.

Text section và data section thường có kích thước tương đối cố định, vì chúng không thay đổi nhiều trong quá trình chạy chương trình.

Ngược lại, stack và heap có thể co giãn động.

Mỗi khi một hàm được gọi, một activation record — hay bản ghi kích hoạt — được đẩy vào stack. Activation record thường chứa tham số hàm, biến cục bộ và địa chỉ trả về. Khi hàm kết thúc, activation record tương ứng được lấy ra khỏi stack.

Heap phát triển khi chương trình cấp phát bộ nhớ động, chẳng hạn như khi tạo object mới. Heap có thể thu lại khi bộ nhớ không còn được sử dụng và được trả về cho hệ thống. Trong các ngôn ngữ có garbage collector như Java, việc thu hồi bộ nhớ trên heap thường do bộ gom rác đảm nhiệm khi object không còn reachable.

Mặc dù stack và heap có thể phát triển về phía nhau, hệ điều hành và runtime phải đảm bảo chúng không chồng lấn lên nhau.

3. Thread

3.1 Từ process đến thread

Những mối quan tâm tương tự — tận dụng tài nguyên, tính công bằng và sự tiện lợi — từng thúc đẩy sự ra đời của process cũng chính là động lực dẫn đến sự phát triển của thread.

Ở mô hình process truyền thống, ta thường hình dung mỗi process là một chương trình đang thực thi với một luồng điều khiển duy nhất — single thread of control. Process có không gian bộ nhớ riêng, tài nguyên riêng và thực hiện các instruction theo một trình tự nhất định.

Mô hình này đủ tốt khi chương trình chỉ cần làm một việc tại một thời điểm.

Tuy nhiên, trong thực tế, một chương trình thường bao gồm nhiều hoạt động có thể diễn ra độc lập hoặc bán độc lập với nhau. Một ứng dụng có thể vừa xử lý giao diện người dùng, vừa đọc file, vừa gửi request qua mạng, vừa thực hiện tính toán ở phía sau.

Nếu tất cả những việc này bị ép đi qua một dòng thực thi duy nhất, chương trình dễ bị kém phản hồi và không tận dụng tốt tài nguyên hệ thống.

Thread ra đời để giải quyết vấn đề đó ở bên trong phạm vi của một process.

Thread cho phép nhiều dòng điều khiển của chương trình cùng tồn tại trong cùng một process. Các thread chia sẻ những tài nguyên ở phạm vi process, chẳng hạn như không gian địa chỉ bộ nhớ, heap, code section và file handles. Nhưng mỗi thread vẫn có phần trạng thái thực thi riêng của nó, bao gồm program counter, register set, stack và các biến cục bộ.

Nhờ cách tổ chức này, nhiều phần khác nhau của cùng một chương trình có thể tiến triển gần như đồng thời. Nếu một thread đang chờ I/O, thread khác vẫn có thể tiếp tục xử lý công việc. Trên hệ thống multicore, nhiều thread trong cùng một process thậm chí có thể được lập lịch chạy song song trên nhiều CPU core.

Vì vậy, nếu process là cách hệ điều hành cô lập và quản lý các chương trình đang chạy, thì thread là cách để một process tự chia nhỏ dòng thực thi của mình thành nhiều luồng điều khiển nhẹ hơn.

Chia sẻ bộ nhớ — sức mạnh và rủi ro

Thread giúp chương trình linh hoạt hơn, phản hồi tốt hơn và có khả năng khai thác parallelism tốt hơn. Nhưng đổi lại, nó cũng đặt ra một thách thức mới: các thread chia sẻ cùng bộ nhớ, nên việc truy cập dữ liệu dùng chung cần được phối hợp cẩn thận bằng các cơ chế synchronization.

3.2 Thread là gì?

Thread là đơn vị thực thi cơ bản bên trong một process. Một process có thể có một thread hoặc nhiều thread.

Mỗi thread có các thành phần riêng như:

  • Thread ID
  • Program counter
  • Register set
  • Stack

Nhưng các thread trong cùng một process chia sẻ nhiều tài nguyên chung, bao gồm:

  • Code section
  • Data section
  • Heap
  • Open files
  • Signals
  • Các tài nguyên hệ điều hành khác thuộc về process

Thread đôi khi được gọi là lightweight process — tiến trình nhẹ — vì nó cũng là một đơn vị thực thi được lập lịch, nhưng nhẹ hơn process do chia sẻ nhiều tài nguyên với các thread khác trong cùng process.

Trong nhiều hệ điều hành hiện đại, scheduler thường lập lịch các đơn vị thực thi tương ứng với thread. Vì vậy, khi nói về CPU scheduling trong các hệ thống hiện đại, thread thường là đơn vị quan trọng hơn process.

3.3 Vì sao cần thread?

Thread cho phép nhiều dòng điều khiển cùng tồn tại bên trong một process. Điều này mang lại một số lợi ích lớn.

Khai thác tốt hơn tài nguyên CPU

Nếu một thread đang chờ I/O, thread khác trong cùng process vẫn có thể tiếp tục chạy. Điều này giúp chương trình tận dụng CPU tốt hơn thay vì bị dừng hoàn toàn.

Ví dụ, một server đang chờ response từ database cho request này vẫn có thể tiếp tục xử lý request khác.

Tăng tính phản hồi

Trong các ứng dụng giao diện người dùng, một thread có thể xử lý UI trong khi thread khác thực hiện tác vụ nền. Nhờ đó, ứng dụng không bị "đơ" khi đang đọc file lớn, gọi network hoặc xử lý dữ liệu nặng.

Đây là lý do nhiều ứng dụng tách UI thread khỏi background worker thread.

Chia nhỏ chương trình tự nhiên hơn

Một số bài toán vốn có nhiều công việc độc lập hoặc bán độc lập.

Ví dụ, một web server có thể dùng nhiều thread để phục vụ nhiều request; trình duyệt có thể dùng thread khác nhau cho rendering, network, JavaScript execution hoặc background tasks.

Thay vì ép mọi thứ vào một dòng thực thi duy nhất, thread cho phép chương trình được tổ chức gần hơn với bản chất của bài toán.

Khai thác parallelism trên multicore

Trên hệ thống multiprocessor hoặc multicore, nhiều thread trong cùng một process có thể được lập lịch chạy đồng thời trên nhiều CPU core.

Đây là cơ sở quan trọng để tăng hiệu năng cho các chương trình parallel.

Concurrency ≠ Parallelism
  • Concurrency = nhiều công việc cùng tiến triển trong cùng một khoảng thời gian.
  • Parallelism = nhiều công việc thực sự chạy cùng lúc trên nhiều core.

Thread có thể hỗ trợ cả concurrency lẫn parallelism, nhưng có đạt được parallelism thực sự hay không còn phụ thuộc vào phần cứng, hệ điều hành, runtime và bản chất workload.

4. User thread, kernel thread và các mô hình ánh xạ

4.1 User thread và kernel thread

Thread có thể được hỗ trợ ở hai cấp độ: user level và kernel level.

User thread được quản lý ở user space, thường bởi một thread library. Kernel có thể không biết trực tiếp đến từng user thread.

Kernel thread được hỗ trợ và quản lý trực tiếp bởi hệ điều hành. Scheduler của hệ điều hành có thể lập lịch trực tiếp cho các kernel thread.

Hầu hết các hệ điều hành hiện đại như Windows, Linux và macOS đều hỗ trợ kernel thread.

Vấn đề quan trọng là: user thread được ánh xạ xuống kernel thread như thế nào? Có ba mô hình phổ biến: many-to-one, one-to-one và many-to-many.

Các mô hình này giúp ta hiểu vì sao có loại thread nhẹ nhưng không tận dụng được multicore, có loại thread tận dụng được multicore nhưng tạo quá nhiều lại tốn kém, và vì sao các runtime hiện đại như JVM lại cố gắng tìm cách cân bằng giữa hai phía.

4.2 Many-to-one

Trong mô hình many-to-one, nhiều user-level thread được ánh xạ vào một kernel thread duy nhất.

Ưu điểm của mô hình này là việc quản lý thread có thể rất nhanh, vì phần lớn thao tác được thực hiện ở user space mà không cần kernel can thiệp.

Nhược điểm rất lớn là nếu một thread thực hiện blocking system call, toàn bộ process có thể bị block. Ngoài ra, vì chỉ có một kernel thread, các user thread không thể thực sự chạy song song trên nhiều CPU core.

Các phiên bản đầu của Java từng sử dụng mô hình tương tự thông qua Green Threads, nhưng mô hình này ngày nay ít được sử dụng vì không tận dụng tốt phần cứng multicore.

4.3 One-to-one

Trong mô hình one-to-one, mỗi user thread được ánh xạ tới một kernel thread tương ứng.

Mô hình này cung cấp concurrency tốt hơn many-to-one. Nếu một thread bị block vì system call, các thread khác vẫn có thể tiếp tục chạy. Nó cũng cho phép nhiều thread chạy song song trên hệ thống multicore.

Nhược điểm là mỗi user thread cần một kernel thread tương ứng. Nếu ứng dụng tạo quá nhiều thread, hệ thống có thể chịu chi phí lớn về bộ nhớ, scheduling và context switching.

4.4 Many-to-many và two-level

Trong mô hình many-to-many, nhiều user-level thread được multiplex lên một số lượng kernel thread nhỏ hơn hoặc bằng.

Mô hình này cố gắng kết hợp ưu điểm của hai mô hình trước. Developer có thể tạo nhiều user thread, trong khi runtime hoặc thread library ánh xạ chúng xuống một số lượng kernel thread phù hợp.

Khi một thread bị block, kernel vẫn có thể schedule kernel thread khác. Đồng thời, hệ thống vẫn có khả năng chạy song song trên nhiều CPU core.

Tuy nhiên, many-to-many khó triển khai hơn. Khi số lượng core trên phần cứng ngày càng tăng và chi phí kernel thread trở nên chấp nhận được hơn, hầu hết hệ điều hành hiện đại có xu hướng sử dụng mô hình one-to-one.

Một biến thể của many-to-many là mô hình two-level, trong đó nhiều user thread vẫn được multiplex lên nhiều kernel thread, nhưng một user thread cụ thể có thể được bind trực tiếp vào một kernel thread.

5. Thread trong Java

5.1 Platform thread và mô hình one-to-one

Java thread chạy bên trong JVM, nhưng JVM không tự mình "giả lập" toàn bộ việc lập lịch thread theo cách tách biệt hoàn toàn với hệ điều hành.

Với platform thread truyền thống, Java thread thường được ánh xạ xuống native thread của hệ điều hành. Vì vậy, khi tạo một platform thread trong Java, ta thường cũng đang tạo hoặc sử dụng một kernel thread tương ứng ở tầng hệ điều hành.

Điều này giúp Java thread có thể tận dụng khả năng parallelism trên hệ thống multicore. Tuy nhiên, nó cũng có nghĩa là thread không hề miễn phí. Tạo quá nhiều platform thread có thể gây ra nhiều chi phí:

  • Tốn bộ nhớ cho stack của 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 hơn khi số lượng request tăng cao.

Ví dụ tạo platform thread trực tiếp:

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

worker.start();

Cách này đơn giản và dễ hiểu, nhưng không phải lúc nào cũng phù hợp trong ứng dụng thực tế. Nếu mỗi request lại tạo một thread mới không kiểm soát, hệ thống có thể nhanh chóng cạn tài nguyên khi tải tăng cao.

5.2 ExecutorService: quản lý task thay vì tự quản lý thread

Đó là lý do trong các ứng dụng Java hiện đại, thay vì tạo thread thủ công quá nhiều, ta thường dùng các abstraction cao hơn như ExecutorService.

ExecutorService giúp tách biệt việc mô tả task khỏi việc trực tiếp quản lý thread.

Thay vì tự tạo từng thread, developer gửi task vào executor. Executor sẽ quyết định dùng thread nào để chạy task đó, có thể là thread mới hoặc thread đã có sẵn trong thread pool.

ExecutorService executor = Executors.newFixedThreadPool(4);

executor.submit(() -> {
    System.out.println("Run task in thread pool");
});

executor.shutdown();

Ở đây, ta không trực tiếp quản lý vòng đời của từng thread. Ta chỉ nói: "Đây là công việc cần chạy." Còn executor chịu trách nhiệm điều phối công việc đó lên các thread trong pool.

Cách tiếp cận này có nhiều lợi ích:

  • Kiểm soát số lượng thread tốt hơn.
  • Tái sử dụng thread thay vì tạo mới liên tục.
  • Giảm chi phí tạo/hủy thread.
  • Tách biệt business logic khỏi thread management.
  • Dễ cấu hình và giám sát hơn trong ứng dụng lớn.

Tuy nhiên, ExecutorService với fixed thread pool vẫn dựa trên platform thread. Điều đó có nghĩa là nếu task chủ yếu là blocking I/O, số lượng thread trong pool vẫn có thể trở thành giới hạn.

Ví dụ, nếu pool có 100 thread và cả 100 thread đều đang chờ database hoặc remote API, request thứ 101 có thể phải chờ, dù CPU thực tế vẫn còn rảnh.

Đây chính là một trong những động lực quan trọng dẫn đến virtual thread.

5.3 Virtual thread: nhiều Java thread, ít carrier thread hơn

Từ các phiên bản Java hiện đại, Java cung cấp thêm virtual thread.

Virtual thread vẫn là một instance của java.lang.Thread, nhưng nó không còn gắn cố định với một OS thread theo kiểu one-to-one như platform thread truyền thống.

Thay vào đó, JVM có thể ánh xạ rất nhiều virtual thread lên một tập nhỏ hơn các platform thread, thường được gọi là carrier threads.

Khi một virtual thread cần chạy, JVM sẽ mount nó lên một carrier thread. Carrier thread này sau đó được hệ điều hành lập lịch như một thread bình thường. Khi virtual thread gặp một thao tác blocking I/O phù hợp, nó có thể unmount khỏi carrier thread, nhường carrier thread đó cho virtual thread khác.

Vì vậy, nếu so với các mô hình multithreading kinh điển, virtual thread trong Java gần nhất với mô hình many-to-many: nhiều virtual thread ở tầng Java được multiplex lên nhiều platform thread ở tầng JVM/OS.

Điểm quan trọng là virtual thread giúp phá vỡ quan hệ one-to-one giữa "một Java thread" và "một OS thread". Nhờ đó, ứng dụng Java có thể tạo ra số lượng rất lớn thread phục vụ các tác vụ blocking I/O mà không phải trả chi phí tương ứng với việc tạo cùng số lượng OS thread.

Ví dụ tạo virtual thread trực tiếp:

Thread.startVirtualThread(() -> {
    System.out.println("Run in virtual thread");
});

Hoặc dùng executor dành cho virtual thread:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        System.out.println("Run task in virtual thread");
    });
}

Virtual thread đặc biệt phù hợp với các ứng dụng có nhiều tác vụ blocking I/O, chẳng hạn như:

  • Web server xử lý nhiều request đồng thời.
  • Service thường xuyên gọi database.
  • Service gọi nhiều remote API.
  • Ứng dụng cần duy trì nhiều kết nối đồng thời.
  • Các workload mà phần lớn thời gian thread ở trạng thái chờ.
Virtual thread không phải thuốc thần

Với các tác vụ CPU-bound nặng, số lượng CPU core vẫn là giới hạn thực sự. Virtual thread giúp giảm chi phí chờ đợi I/O, nhưng không làm một CPU core có thể tính toán nhiều việc nặng cùng lúc theo nghĩa vật lý. Nó không làm concurrency biến mất độ phức tạp, nhưng làm cho mô hình "thread per request" trở nên khả thi hơn nhiều trong các ứng dụng I/O-bound hiện đại.

6. Kết luận

Process và thread là hai khái niệm nền tảng trong hệ điều hành và lập trình concurrent.

Process giúp hệ điều hành cô lập và quản lý các chương trình đang chạy. Mỗi process có không gian địa chỉ, trạng thái thực thi và tài nguyên riêng.

Thread cho phép nhiều luồng điều khiển cùng tồn tại bên trong một process. Các thread chia sẻ tài nguyên của process, nhưng mỗi thread có program counter, register set và stack riêng.

Sự chia sẻ này giúp thread nhẹ hơn process và thuận tiện hơn cho việc phối hợp dữ liệu. Nhưng nó cũng tạo ra rủi ro: nếu nhiều thread cùng truy cập dữ liệu dùng chung mà không có synchronization, chương trình có thể sinh ra lỗi khó đoán như race condition.

Trong Java, JVM chạy như một process của hệ điều hành. Bên trong process đó, Java có thể sử dụng platform thread, thread pool, executor và virtual thread để tổ chức việc thực thi chương trình.

Platform thread truyền thống thường gần với mô hình one-to-one giữa Java thread và OS thread. Trong khi đó, virtual thread đưa Java đến gần hơn với mô hình many-to-many, nơi rất nhiều virtual thread có thể được JVM multiplex lên một số lượng nhỏ hơn các carrier thread.

Hiểu rõ process, thread và cách chúng liên hệ với hệ điều hành là nền tảng quan trọng để học sâu hơn về Java concurrency, synchronization, thread pool, executor, virtual thread, parallelism và các mô hình lập trình bất đồng bộ hiện đại.

Tự kiểm tra
Q1
Phân biệt program và process. Vì sao chạy cùng một chương trình hai lần lại là hai process khác nhau?

Program là thực thể thụ động — một file chứa danh sách instruction nằm trên đĩa (executable file). Process là thực thể chủ động — có program counter, trạng thái thực thi, không gian bộ nhớ riêng và tập tài nguyên do OS cấp phát.

Chạy cùng một chương trình hai lần tạo ra hai process vì mỗi lần nạp executable vào bộ nhớ, OS dựng một không gian địa chỉ và tập tài nguyên riêng. Chúng có thể dùng chung text section, nhưng data, heap và stack là độc lập — như mở hai cửa sổ trình duyệt cùng lúc.

Q2
Hai thread cùng một process dùng chung những gì, giữ riêng những gì?

Chung: code section, data section, heap (mọi object), open files, signals và các tài nguyên OS thuộc process.

Riêng: thread ID, program counter, register set và stack (biến cục bộ).

Vì stack riêng nên biến cục bộ của thread này thread khác không chạm tới — mặc nhiên an toàn. Vì heap chung nên object được chia sẻ; đây vừa là sức mạnh (chia dữ liệu không cần copy) vừa là cội nguồn của race condition khi thiếu synchronization.

Q3
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. Hệ quả: (1) một blocking system call ở một thread làm block cả process; (2) không thể chạy song song trên nhiều CPU core — chỉ có một kernel thread được scheduler lập lịch.

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).

Q4
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 tình huống này 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 mô hình 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 ý: điều này 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 cơ bản — chạy song song, start/join/interrupt

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