Process và Thread: khác nhau ở đâu?

4 câu hỏi~20 phútTừ khóa: process, thread, heap, stack, context switch, platform/virtual thread, thread-per-request, IPC

Thread và process khác nhau thế nào? Mình hiểu là khi chạy, Java là một process, bên trong có nhiều thread: một số do JVM tạo, một số do code của mình tạo. Đúng không?

Bạn hiểu đúng rồi. Nói ngắn: process là một chương trình đang chạy, có vùng nhớ và tài nguyên riêng. Thread là một luồng thực thi bên trong process, dùng chung vùng nhớ đó.

Cả chương Concurrency mình sẽ dùng chung một ví dụ để các bài nối vào nhau:

Client ──HTTP──> Order Service (Spring Boot, Tomcat) ──HTTP──> Payment Service / Bank
                     │
                     └── MySQL

Order Service nhận request tạo đơn, lưu DB, gọi Payment để trừ tiền. Bài này trả lời câu đầu tiên: khi chạy Order Service, trong máy thực sự có gì?

1. Bài toán

2. Vấn đề

Nếu chỉ có một luồng chạy (như hàm main đơn giản), request thứ 2 phải đợi request 1 xong, kể cả lúc request 1 chỉ đang ngồi chờ Payment trả lời:

1 luồng duy nhất:
Request 1 ████ chờ Payment 2s ████ xong
Request 2                              ████ chờ 2s ████ xong
Request 3                                                   ...
→ 500 request × 2s = hơn 16 phút cho người cuối

Còn nếu chạy mỗi request bằng một process riêng thì quá nặng: mỗi process phải có vùng nhớ riêng, JVM riêng, và muốn chia sẻ cache hay connection pool thì phải qua IPC.

3. Giải pháp: một process, nhiều thread

Khi bạn chạy java -jar, OS tạo ra một Java process. Bên trong process đó có rất nhiều thread:

OS
├── Java Process (order-service)
│   ├── Memory / Heap
│   ├── File handles, network connections
│   └── Threads: main, GC, JIT, http-nio-8080-exec-1..200, worker của bạn...
├── Chrome Process
└── MySQL Process

Tạo thêm thread thì vẫn là 1 process, chỉ là process đó có thêm thread. Một ý đáng nhớ: CPU thực tế chạy thread, còn process chủ yếu là "container" chứa tài nguyên cho các thread.

Trong JVM, mỗi thread có stack riêng, tất cả thread dùng chung heap:

4. Code từng bước

Bước 1: xem process và thread đang có.

public class Main {
    public static void main(String[] args) {
        System.out.println("PID = " + ProcessHandle.current().pid());
        System.out.println("Thread = " + Thread.currentThread().getName());   // main
        Thread.getAllStackTraces().keySet()
              .forEach(t -> System.out.println(" - " + t.getName()));        // GC, Finalizer, ...
    }
}

Bước 2: Order Service chạy controller trên thread nào? Spring MVC chạy trên Tomcat theo mô hình thread-per-request: mỗi request được một thread trong pool của Tomcat nhận và xử lý từ đầu đến cuối (filter → controller → service → repository → response).

@RestController
public class OrderController {
    @GetMapping("/whoami")
    public String whoami() {
        return Thread.currentThread().getName();   // ví dụ: http-nio-8080-exec-3
    }
}

Bước 3: config pool của Tomcat.

# application.properties
server.tomcat.threads.max=200
server.tomcat.threads.min-spare=10
# Spring Boot 3.2+ với Java 21: xử lý request bằng virtual thread
spring.threads.virtual.enabled=true

Bước 4: chỗ nào chung, chỗ nào riêng trong code thật.

@Service
public class OrderService {
    private int orderCount = 0;          // field của singleton bean → trên heap, MỌI thread dùng chung

    public void create(OrderRequest req) {
        int total = req.qty() * 10;      // biến local → trên stack của thread hiện tại, an toàn
        orderCount++;                    // nguy hiểm: nhiều thread cùng sửa (bài 3.4)
    }
}

Bước 5: test.

curl localhost:8080/whoami   # http-nio-8080-exec-1
curl localhost:8080/whoami   # http-nio-8080-exec-2  (thread khác, cùng process)
jcmd <pid> Thread.print       # thread dump: xem toàn bộ thread của process

5. Cách hoạt động

                Order Service = 1 Java process
┌──────────────────────────────────────────────────────┐
│ HEAP (dùng chung)                                    │
│   OrderService bean, orderCount, cache, DB pool...   │
├──────────────────────────────────────────────────────┤
│ exec-1  [stack: req, total]   → Request A            │
│ exec-2  [stack: req, total]   → Request B            │
│ ...                                                  │
│ exec-200[stack: req, total]   → Request X            │
│ GC, JIT, main ...                                    │
└──────────────────────────────────────────────────────┘
        ▲                    ▲
     8 core CPU: OS lập lịch, mỗi lúc 1 core chạy 1 thread
     nhiều thread hơn core → context switch

Thread không miễn phí. Mỗi platform thread tốn:

Với việc nặng CPU, 8 core mà chạy 1000 thread thì không nhanh hơn 8 thread, thậm chí chậm hơn. Nên Tomcat mới giới hạn 200 thread: nếu 200 request cùng chờ Payment chậm, request thứ 201 phải xếp hàng.

java.lang.Thread thông thường là platform thread, ánh xạ 1:1 với một OS thread. Java 21 có thêm virtual thread: thread nhẹ do JVM quản lý, nhiều virtual thread chạy chung trên vài OS thread (carrier). Khi virtual thread bị block vì I/O, JVM tháo nó khỏi carrier, nhường chỗ cho virtual thread khác.

Thread platform = Thread.ofPlatform().start(() -> System.out.println("OS thread"));
Thread virtual  = Thread.ofVirtual().start(() -> System.out.println("virtual thread"));
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> { Thread.sleep(1000); return null; });  // 10k task vẫn ổn
    }
}

6. So sánh

ProcessThread
Bộ nhớRiêng, process khác không đọc đượcChung heap, stack riêng
Chi phí tạoNặngNhẹ hơn nhiều (nhưng không miễn phí)
Giao tiếpQua IPC: socket, pipe, file…Đọc ghi chung object trên heap
Một cái lỗi nặngProcess khác không ảnh hưởngCó thể kéo sập cả process (ví dụ OutOfMemoryError)
Rủi ro chínhTốn tài nguyênRace condition khi dùng chung dữ liệu

7. Khi nào nên dùng / không nên dùng

Thread có thể quản lý một process khác không?

Bạn hiểu gần đúng, chỉ cần chỉnh một điểm: thread không chứa process. Quan hệ luôn là Process → Thread, không có Thread → Process.

Nhưng một thread có thể nhờ OS tạo hoặc điều khiển một process khác. Ví dụ Order Service gọi script Python xuất hóa đơn PDF:

Process python = new ProcessBuilder("python", "invoice.py")
        .inheritIO()
        .start();
int exitCode = python.waitFor();   // thread hiện tại chờ process kia chạy xong
Java Process
  └── Thread
        │ yêu cầu OS tạo
        ▼
Python Process      ← process độc lập, do OS quản lý
  ├── Thread
  └── Thread

Python process không nằm trong Java thread. Nó có bộ nhớ riêng, Java chỉ giao tiếp với nó qua stdin/stdout, exit code (tức là IPC). Java process chết thì Python process vẫn có thể sống tiếp.

Giới hạn số thread của application và của Tomcat là hai con số khác nhau, hay là một?

Là 2 con số khác nhau. Không có kiểu "Java application max threads = 200" rồi Tomcat và ExecutorService cùng chia quota đó. Order Service là 1 JVM process, bên trong có nhiều nhóm thread:

Order Service JVM Process
│
├── Tomcat Thread Pool          (server.tomcat.threads.max = 200)
│   ├── http-nio-8080-exec-1
│   └── ... exec-200
│
├── ExecutorService của bạn     (bạn tự cấu hình, ví dụ 50)
│   ├── pool-1-thread-1
│   └── ...
│
├── Scheduler threads
├── DB connection pool related threads
├── GC threads
└── JVM/internal threads

Cấu hình Tomcat 200, pool của bạn 50, scheduler 10 thì process hoàn toàn có thể có trên 260 thread. Giới hạn cuối cùng là OS và bộ nhớ (mỗi thread một stack), chứ không có một con số mặc định mà mọi pool phải chia nhau.

Vậy sao không tạo 100.000 thread luôn? Vì như ở trên: stack, bookkeeping của OS và context switch. Và nếu thread chủ yếu ngồi chờ I/O (chờ Payment, chờ DB) thì có nhiều thread cũng chỉ là nhiều thread ngồi chờ. Bài thread pool và CompletableFuture sẽ quay lại chuyện này.

Phỏng vấn hỏi process và thread thì trả lời sao?

Câu 1: Process và thread khác nhau thế nào?

🇻🇳 Tiếng Việt:

Process là chương trình đang chạy, có không gian bộ nhớ riêng, giao tiếp với nhau qua IPC. Thread là luồng thực thi trong process, các thread dùng chung heap nhưng có stack riêng, nên tạo và chuyển đổi rẻ hơn nhưng phải lo đồng bộ dữ liệu chung. Khi chạy app Java, OS tạo một Java process, bên trong có thread của JVM, của framework như Tomcat và của code mình.

🇬🇧 English:

A process is a running program with its own memory space; processes talk to each other through IPC. A thread is a unit of execution inside a process. Threads share the heap but each has its own stack, so they are cheaper to create and switch, but shared data must be synchronized. A Java app runs as one process containing JVM threads, framework threads like Tomcat's, and our own threads.

Câu 2: Có nên tạo thật nhiều thread không?

🇻🇳 Tiếng Việt:

Không. Platform thread trong Java ánh xạ 1:1 với OS thread, mỗi thread tốn stack khoảng 1MB và gây context switch khi nhiều hơn số core. Nên mình dùng thread pool có giới hạn. Với tác vụ I/O nhiều, Java 21 có virtual thread rất nhẹ, khi block I/O sẽ nhả OS thread cho việc khác.

🇬🇧 English:

No. A Java platform thread maps 1:1 to an OS thread; each one costs roughly 1MB of stack and causes context switching once there are more threads than cores. So we use bounded thread pools. For I/O-heavy work, Java 21 virtual threads are very cheap and release the OS thread while blocked on I/O.

Câu 3: Spring Boot xử lý request bằng thread nào?

🇻🇳 Tiếng Việt:

Spring MVC trên Tomcat dùng mô hình thread-per-request: mỗi request được một thread trong pool của Tomcat xử lý, mặc định tối đa 200. Vì nhiều request chạy song song trên cùng singleton bean, field của bean là dữ liệu dùng chung và phải thread-safe, còn biến local thì an toàn vì nằm trên stack riêng.

🇬🇧 English:

Spring MVC on Tomcat uses thread-per-request: each request is handled by one thread from Tomcat's pool, 200 max by default. Since many requests run concurrently on the same singleton beans, bean fields are shared state and must be thread-safe, while local variables are safe because they live on each thread's own stack.

Cách nhớ ngắn gọn


Tự kiểm tra

Biến local trong method có bị race condition không?

Không, nó nằm trên stack riêng của từng thread. Nhưng nếu biến local trỏ tới một object dùng chung trên heap thì object đó vẫn có thể bị.

Context switch là gì, vì sao tốn?

Là việc OS dừng thread đang chạy, lưu trạng thái, nạp thread khác lên core. Tốn thời gian lưu/nạp và làm giảm hiệu quả cache CPU.

Platform thread khác virtual thread thế nào?

Platform thread = 1 OS thread, nặng. Virtual thread do JVM quản lý, rất nhẹ, khi block I/O sẽ nhả OS thread cho virtual thread khác.

Vì sao field trong @Service dễ gây lỗi concurrency?

Bean là singleton, mọi request thread dùng chung một instance, nên field có thể bị nhiều thread sửa cùng lúc.

ProcessBuilder.start() tạo ra thread hay process?

Process mới, độc lập, do OS quản lý. Java chỉ giao tiếp với nó qua IPC (stdin/stdout, exit code).