Java Threads và Concurrency: Từ CPU, Thread Pool đến Virtual Threads
Hãy tưởng tượng một service nhận 10.000 request cùng lúc.
Một số request cần CPU để parse JSON, hash hoặc nén dữ liệu. Một số đang chờ database. Một số đang chờ HTTP API bên ngoài trả lời sau vài trăm mili-giây.
Câu hỏi đầu tiên là:
Có nên tạo 10.000 thread?
Nếu không, tại sao? Và nếu Java Virtual Threads cho phép tạo hàng trăm nghìn hoặc hàng triệu thread, tại sao concurrency vẫn không thể tăng vô hạn?
Để trả lời, hãy nhìn toàn bộ stack:
Application Code
↓
Java Threads
↓
JVM
↓
Operating System Scheduler
↓
CPU Cores
↓
External Resources
(Database, API, Network, Files...)
Hầu hết vấn đề concurrency thực chất là một câu hỏi:
Bottleneck thật sự nằm ở đâu?
CPU? Database connection? HTTP API? Memory? Lock contention? Thread pool? Queue?
Thêm thread chỉ giúp khi nó giải quyết đúng bottleneck. Nếu không, bạn chỉ chuyển bottleneck sang một nơi khác.
Bài viết này đi từ nền tảng đến production:
- Concurrency và parallelism khác nhau thế nào?
- Platform thread thực sự là gì?
- Vì sao
count++không thread-safe? synchronized,Atomic*,Lockvàvolatilekhác nhau ra sao?- Thread pool hoạt động thế nào dưới quá tải?
- Vì sao thêm nhiều thread đôi khi làm ứng dụng chậm hơn?
- Những lỗi concurrency phổ biến ở production.
- Virtual Threads thay đổi điều gì?
- Vì sao Virtual Threads vẫn cần giới hạn concurrency?
Các ví dụ đi cùng repository java-lab, chạy trên Java 21+. Các số liệu trong bài là kết quả mẫu từ các ví dụ, không phải benchmark tuyệt đối.
Source Code & Examples
Bạn có thể đọc bài viết đầy đủ tại:
https://java-guru.pages.dev/en/posts/java-core/java-threads-from-thread-pools-to-virtual-threads/
Toàn bộ ví dụ trong bài viết được triển khai trong repository:
Repository bao gồm các ví dụ thực tế về:
- Tạo và quản lý
Thread,Runnable,Callable,join() - Thread lifecycle
- Race condition và shared state
synchronized,AtomicInteger,ReentrantLock,volatileThreadPoolExecutor, bounded queue và rejection policies- CPU-bound vs I/O-bound workloads
- Deadlock, starvation, thread pool exhaustion và
ThreadLocalleak - Virtual Threads
- Giới hạn concurrency bằng
Semaphore - Producer–Consumer và backpressure
- Graceful shutdown
Bạn có thể đọc bài viết song song với source code để xem từng khái niệm hoạt động thực tế.
1. Vì sao chúng ta cần Thread?
Một chương trình tuần tự chạy như sau:
Task A
↓
Task B
↓
Task C
Nhưng backend thực tế thường có nhiều request ở các trạng thái khác nhau:
Request A ── chờ database
Request B ── đang tính toán
Request C ── chờ HTTP API
Request D ── đọc file
Nếu chương trình xử lý hoàn toàn tuần tự, CPU có thể ngồi rảnh trong khi một request chờ database.
Đó là lý do thread tồn tại:
Cho phép công việc khác tiếp tục trong khi một công việc đang chờ.
CPU có thể thực hiện hàng tỷ chỉ thị mỗi giây, trong khi một database hoặc HTTP request có thể mất vài mili-giây hoặc vài giây.
Thread giúp chồng lấp thời gian chờ đó.
Nhưng thread không miễn phí. Nó có bộ nhớ, chi phí scheduler, context switching và đặc biệt là shared mutable state.
Vì vậy, concurrency luôn là bài toán đánh đổi:
Nhiều concurrency hơn
↓
CPU ít bị bỏ phí hơn
↓
Nhưng nhiều shared state, scheduling và resource contention hơn
2. Concurrency và Parallelism
Hai khái niệm này thường bị dùng lẫn nhau.
Concurrency
Nhiều tác vụ cùng tiến triển trong các khoảng thời gian chồng lấn.
Trên một CPU:
Thread A: |--A1--| |--A2--| |--A3--|
Thread B: |--B1--| |--B2--|
CPU không nhất thiết chạy hai task cùng lúc. Nó chuyển qua lại giữa chúng.
Concurrency đặc biệt hữu ích khi task thường xuyên chờ I/O.
Parallelism
Nhiều tác vụ thực sự chạy cùng thời điểm trên nhiều CPU core:
Core 1: |------Task A------|
Core 2: |------Task B------|
Điểm quan trọng:
Concurrency giúp che giấu thời gian chờ. Parallelism giúp tận dụng nhiều CPU core.
Tạo 1.000 thread trên máy 4 core không tạo ra 1.000 đơn vị tính toán song song.
Nhiều nhất chỉ có khoảng 4 thread thực sự chạy trên CPU tại một thời điểm. Những thread còn lại đang:
- chờ I/O;
- chờ lock;
- sleeping;
- hoặc chờ scheduler.
Tạo thread không tạo thêm CPU core.
2.1 CPU-bound và I/O-bound
Đây là câu hỏi đầu tiên trước khi thiết kế concurrency.
CPU-bound
Task dành phần lớn thời gian để tính toán:
- hashing;
- compression;
- encryption;
- image processing;
- tính toán lớn.
[===== compute =====][===== compute =====]
Bottleneck là CPU.
Thêm thread chỉ giúp đến gần số CPU core.
I/O-bound
Task dành phần lớn thời gian chờ:
- database;
- HTTP API;
- file;
- network.
[wait 95ms]
[work 5ms]
Trong khi Task A chờ, CPU có thể chạy Task B.
Đây là nơi concurrency mang lại lợi ích lớn.
Mô hình đơn giản:
CPU-bound → giới hạn bởi số core
I/O-bound → giới hạn bởi latency và số tài nguyên bên ngoài
3. Platform Thread Thực Sự Là Gì?
Một Java platform thread không chỉ là một object.
Về mặt khái niệm:
Java Platform Thread
↓
JVM
↓
Native / OS Thread
↓
Operating System Scheduler
↓
CPU Core
Platform thread thường có quan hệ gần như 1:1 với OS thread.
Một thread cần:
- stack;
- program counter;
- register state;
- scheduling metadata;
- kernel resources.
Vì vậy, tạo rất nhiều platform thread có chi phí:
More threads
↓
More memory
↓
More scheduler work
↓
More context switches
↓
More cache disruption
Đó là lý do mô hình sau không mở rộng tốt:
new Thread(task).start();
cho mỗi request.
Ba thread thì không có vấn đề.
Ba nghìn hoặc mười nghìn thread có thể trở thành vấn đề về memory và scheduling.
Runnable và Callable
Runnable:
void run()
- không trả về kết quả;
- không ném checked exception.
Callable<V>:
V call() throws Exception
- trả về giá trị;
- có thể ném exception.
Ví dụ:
Callable<Integer> task = () -> {
Thread.sleep(100);
return 42;
};
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<Integer> future = executor.submit(task);
System.out.println(future.get());
} finally {
executor.shutdown();
}
Future.get() sẽ block caller cho đến khi kết quả sẵn sàng.
Một chi tiết quan trọng:
Exception bên trong task có thể được giữ trong
Future.
Nếu bạn submit task nhưng không bao giờ kiểm tra Future, failure có thể bị bỏ qua.
4. start() và run() Không Giống Nhau
Thread thread = new Thread(task, "worker");
thread.run();
thread.start();
Hai dòng này có ý nghĩa hoàn toàn khác.
run()
Chỉ là một method call bình thường:
main
↓
run()
↓
task chạy trên main
start()
Yêu cầu JVM tạo một execution thread mới:
main
↓
start()
↓
new thread
↓
JVM gọi run() trên thread mới
Ví dụ:
Runnable task = () ->
System.out.println(Thread.currentThread().getName());
Thread thread = new Thread(task, "new-thread");
thread.run(); // main
thread.start(); // new-thread
Sau start(), thứ tự thực thi trở thành nondeterministic.
Bạn không thể giả định:
A luôn chạy trước B
Scheduler quyết định điều đó.
Đây là một trong những lý do concurrency bug khó tái hiện:
Code đúng trong một lần chạy không có nghĩa là nó đúng trong mọi interleaving.
5. Thread Lifecycle
Các trạng thái thread rất quan trọng khi đọc thread dump:
NEW
↓ start()
RUNNABLE
├── BLOCKED
├── WAITING
├── TIMED_WAITING
↓
TERMINATED
NEW
Thread đã được tạo nhưng chưa gọi start().
RUNNABLE
Thread đang chạy hoặc sẵn sàng chạy.
Quan trọng:
RUNNABLEkhông có nghĩa chắc chắn đang chạy trên CPU.
Nó có thể đang chờ scheduler cấp CPU time.
BLOCKED
Đang chờ một synchronized monitor.
Thread A giữ lock
↓
Thread B cần lock đó
↓
Thread B → BLOCKED
WAITING
Chờ một sự kiện mà không có timeout.
Ví dụ:
Object.wait();Thread.join();LockSupport.park().
TIMED_WAITING
Chờ có timeout:
Thread.sleep();join(timeout);await(timeout).
TERMINATED
run() đã kết thúc hoặc thread bị terminate vì exception.
Đọc thread dump
Thread state thường cho bạn hướng debug:
Nhiều BLOCKED
→ lock contention
Nhiều RUNNABLE
→ có thể CPU saturation
Nhiều WAITING/TIMED_WAITING
→ đang chờ queue, future hoặc external event
Thread states không chỉ là lý thuyết.
Chúng là một trong những công cụ đầu tiên khi production bị treo.
6. Race Condition: Vì Sao count++ Bị Sai?
Giả sử:
private int count;
public void increment() {
count++;
}
Nhiều developer nhìn thấy một câu lệnh.
CPU nhìn thấy:
Read count
↓
Add 1
↓
Write count
Hai thread có thể interleave:
Thread A đọc 0
Thread B đọc 0
Thread A tính 1
Thread B tính 1
Thread A ghi 1
Thread B ghi 1
Kỳ vọng:
2
Thực tế:
1
Một increment đã bị mất.
Đây là race condition.
Điều nguy hiểm là lỗi không phải lúc nào cũng xảy ra.
Một lần chạy có thể đúng.
Lần sau có thể sai.
Production có nhiều CPU, nhiều request và nhiều interleaving hơn nên bug dễ xuất hiện hơn.
6.1 Ba vấn đề khác nhau trong concurrency
Thread safety không chỉ là một vấn đề.
Có ít nhất ba đảm bảo cần phân biệt:
Atomicity
Một operation không bị chia nhỏ theo cách thread khác có thể xen vào.
count++ không atomic.
Visibility
Thread A ghi dữ liệu nhưng thread B có thể không thấy nó đúng lúc nếu không có synchronization phù hợp.
Ordering
CPU và JIT có thể reorder một số thao tác miễn là single-thread semantics vẫn đúng.
Vì vậy:
Một giải pháp cho visibility chưa chắc giải quyết atomicity.
Đó là lý do cần hiểu volatile, Atomic* và lock khác nhau thế nào.
7. Java Memory Model và Synchronization
Java Memory Model định nghĩa khi nào thay đổi của một thread được nhìn thấy bởi thread khác.
Bạn không cần thuộc toàn bộ specification.
Mô hình tư duy thực tế là:
Thread A view of memory
↕
Synchronization
↕
Thread B view of memory
Các cơ chế như:
synchronized;volatile;Atomic*;Lock;
tạo ra các quy tắc visibility và ordering cụ thể.
Khái niệm trung tâm là happens-before.
Ví dụ đơn giản:
Thao tác xảy ra trước khi thread A nhả một lock sẽ trở nên visible với thread B sau khi B acquire cùng lock đó.
7.1 synchronized
public synchronized void increment() {
count++;
}
synchronized cung cấp:
- Mutual exclusion — chỉ một thread vào critical section.
- Visibility — thay đổi được đồng bộ giữa các thread.
Thread A
↓ acquire lock
modify shared state
↓ release lock
↓
Thread B
↓ acquire same lock
sees synchronized state
Nhược điểm:
- lock contention;
- thread phải chờ;
- không có timeout;
- không đảm bảo fairness.
Critical section càng lớn, contention càng cao.
Vì vậy:
Chỉ lock phần nhỏ nhất cần thiết để bảo vệ invariant.
7.2 AtomicInteger
private final AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
Về mặt khái niệm, nó dùng CAS:
Read current value
↓
Compute new value
↓
Compare-And-Swap
↓
Failed? Retry
Ví dụ:
A: CAS(5 → 6) → success
B: CAS(5 → 6) → failed
B: retry
B: CAS(6 → 7) → success
Ưu điểm:
- phù hợp với state đơn giản;
- không cần critical section lớn;
- thường hiệu quả dưới contention nhẹ.
Nhưng AtomicInteger không thay thế lock khi bạn cần bảo vệ invariant giữa nhiều field.
Ví dụ:
Check account A
Check account B
Transfer money
Update A
Update B
Đây là một operation logic lớn hơn một atomic variable.
7.3 ReentrantLock
Dùng khi bạn cần nhiều quyền kiểm soát hơn:
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
ReentrantLock hỗ trợ:
tryLock();- timeout;
- fairness;
- conditions;
- kiểm soát lock tường minh.
Ví dụ:
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// work
} finally {
lock.unlock();
}
}
Điểm quan trọng nhất:
Luôn
unlock()trongfinally.
Nếu exception xảy ra và lock không được release, các thread khác có thể chờ mãi mãi.
Dùng ReentrantLock khi cần timeout hoặc control đặc biệt. Với critical section đơn giản, synchronized thường ít lỗi hơn.
7.4 volatile: Visibility, không phải Atomicity
private volatile int count;
Điều này không làm:
count++;
trở thành atomic.
Nó vẫn là:
read
add
write
volatile phù hợp với các trường hợp như:
private volatile boolean running = true;
Một thread:
while (running) {
// work
}
Thread khác:
running = false;
Ở đây vấn đề chính là visibility.
Quy tắc thực dụng:
volatile
→ flags / visibility
Atomic*
→ atomic read-modify-write trên state đơn giản
synchronized / Lock
→ critical section và invariant phức tạp
8. Thread Pool: Không Chỉ Là Tái Sử Dụng Thread
Một cách đơn giản để xử lý task là:
new Thread(task).start();
Nhưng với 10.000 request:
10,000 tasks
↓
10,000 OS threads
Điều này gây:
- chi phí tạo thread;
- memory stack;
- scheduler overhead;
- context switching;
- nguy cơ cạn native threads.
Thread pool giải quyết bằng cách:
Tasks
↓
Queue
↓
Worker Pool
Một pool làm bốn việc:
- Tái sử dụng worker threads.
- Giới hạn concurrency.
- Giới hạn resource consumption.
- Xác định hành vi khi overload.
Đây là ý tưởng quan trọng:
Thread pool là một cơ chế quản lý tài nguyên.
Không chỉ là tiện ích để chạy async.
9. ThreadPoolExecutor: Core → Queue → Max → Reject
ThreadPoolExecutor xử lý task mới theo thứ tự:
New Task
│
├─ workers < corePoolSize?
│ → create worker
│
├─ queue has space?
│ → enqueue
│
├─ workers < maximumPoolSize?
│ → create more workers
│
└─ otherwise
→ reject
Thứ tự này cực kỳ quan trọng.
Queue được kiểm tra trước khi pool tăng từ core lên max.
Ví dụ:
new ThreadPoolExecutor(
2,
4,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
threadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
Task có thể đi như sau:
Task 1 → worker 1
Task 2 → worker 2
Task 3 → queue
Task 4 → queue
Task 5 → worker 3
Task 6 → worker 4
Task 7 → rejected
Đó là:
core
↓
queue
↓
max
↓
rejection
9.1 Unbounded Queue là một cái bẫy
Với queue không giới hạn:
Tasks keep coming
↓
Queue keeps growing
↓
Memory increases
↓
Latency increases
↓
GC pressure
↓
Possible failure
Một queue không loại bỏ overload.
Nó chỉ cất overload vào bộ nhớ.
Với LinkedBlockingQueue không giới hạn, maximumPoolSize có thể gần như không bao giờ được sử dụng.
Task chỉ tiếp tục vào queue.
Vì vậy, production cần suy nghĩ rõ:
- queue capacity;
- max threads;
- rejection policy;
- upstream backpressure.
9.2 Rejection Policy
Khi pool và queue đều đầy, hệ thống phải quyết định nói gì với task mới.
AbortPolicy
Reject
→ RejectedExecutionException
Phù hợp khi muốn fail fast.
CallerRunsPolicy
Task chạy trên thread submit:
Producer submits task
↓
Pool full
↓
Producer runs task itself
↓
Producer becomes slower
Đây là một dạng backpressure tự nhiên.
DiscardPolicy
Bỏ task im lặng.
Thường nguy hiểm vì mất dữ liệu mà không ai biết.
9.3 Shutdown
Executor phải có vòng đời rõ ràng:
executor.shutdown();
if (!executor.awaitTermination(800, TimeUnit.MILLISECONDS)) {
executor.shutdownNow();
}
Ý nghĩa:
shutdown()
→ stop accepting new tasks
awaitTermination()
→ give current work time to finish
shutdownNow()
→ interrupt running tasks and return queued tasks
Interrupt chỉ hoạt động tốt nếu task tôn trọng interruption.
Production shutdown không phải chỉ là "kill process".
Task cần có cơ hội:
- đóng socket;
- cleanup;
- rollback;
- release resources.
10. Hiệu Năng: Khi Thêm Thread Làm Hệ Thống Chậm Hơn
Context switch không miễn phí.
Khi CPU chuyển từ Thread A sang Thread B:
Save A state
↓
Scheduler bookkeeping
↓
Cache/TLB disruption
↓
Restore B state
↓
Continue B
Nếu có quá nhiều runnable thread:
More threads
↓
More scheduling
↓
More context switches
↓
Less useful work
Vì vậy:
Số thread đúng không phải là số lớn nhất. Đó là số đủ để giữ tài nguyên hữu ích bận.
10.1 CPU-bound
Với workload thuần tính toán:
1 thread
↓
slow
≈ number of cores
↓
maximum useful parallelism
far above cores
↓
little improvement or degradation
Một quy tắc thực dụng:
CPU-bound pool ≈ number of CPU cores
Hoặc đôi khi cores + 1.
Thêm 100 thread cho máy 12 core không tạo thêm 88 core.
10.2 I/O-bound
Với workload:
Compute: 5 ms
Wait: 95 ms
Thread dành phần lớn thời gian không dùng CPU.
Do đó nhiều thread hơn số core có thể hữu ích.
Một công thức heuristic phổ biến:
threads ≈ cores × (1 + wait / compute)
Nhưng đây chỉ là điểm bắt đầu.
Nếu latency external service thay đổi, con số phù hợp cũng thay đổi.
Vì vậy:
Đo workload thật quan trọng hơn tin tuyệt đối vào một công thức.
10.3 Oversubscription
Quá nhiều thread runnable gây:
- scheduler overhead;
- context switching;
- cache disruption.
Mô hình:
Too few threads
→ CPU idle
Enough threads
→ good utilization
Too many threads
→ scheduler becomes the bottleneck
Đây là lý do "tăng pool size để tăng performance" đôi khi làm tình hình tệ hơn.
11. Những Lỗi Concurrency Thường Gặp ở Production
11.1 Deadlock
Ví dụ kinh điển:
Thread A holds Lock 1
Thread A waits for Lock 2
Thread B holds Lock 2
Thread B waits for Lock 1
Không exception.
Không crash.
Chỉ treo.
Deadlock xảy ra khi bốn điều kiện Coffman cùng tồn tại:
- Mutual Exclusion.
- Hold and Wait.
- No Preemption.
- Circular Wait.
Cách phòng thủ quan trọng nhất:
Luôn acquire locks theo cùng một thứ tự toàn cục.
Ví dụ:
Luôn Lock A trước
sau đó Lock B
Không có:
Code path 1: A → B
Code path 2: B → A
Khi debug production, thread dump thường là công cụ quan trọng nhất.
11.2 Starvation
Deadlock nghĩa là không ai tiến triển.
Starvation nghĩa là một số thread vẫn tiến triển, nhưng thread khác có thể bị bỏ đói.
Ví dụ:
Greedy thread
→ repeatedly acquires lock
Other threads
→ repeatedly lose
synchronized không đảm bảo fairness.
Nếu fairness quan trọng:
new ReentrantLock(true)
có thể phù hợp, với trade-off về throughput.
11.3 Thread Pool Exhaustion
Một pool nhỏ có một task bị block lâu:
Slow task occupies worker
↓
Other tasks queue
↓
Latency increases
↓
More requests arrive
↓
Queue grows
↓
Memory pressure
↓
Timeouts
↓
Retries
↓
Cascading failure
Đây là lý do mọi external call nên có timeout.
Không timeout nghĩa là một thread có thể bị chiếm vô hạn.
Phòng thủ:
- timeout;
- bounded queue;
- rejection;
- bulkhead;
- giới hạn concurrency.
11.4 ThreadLocal Leak trong Pool
ThreadLocal gắn với thread, không gắn với task.
Trong pool:
Worker runs Task A
→ ThreadLocal = A data
Worker reused for Task B
→ A data can still exist
Vì worker sống lâu, dữ liệu có thể:
- gây memory leak;
- bị task sau nhìn thấy;
- gây cross-request contamination.
Mẫu an toàn:
try {
threadLocal.set(value);
// work
} finally {
threadLocal.remove();
}
Quy tắc:
Pooled thread được tái sử dụng, nên ThreadLocal cần cleanup rõ ràng.
11.5 Exception Bị Mất
executor.submit(() -> {
throw new RuntimeException("failed");
});
Exception có thể được giữ trong Future.
Nếu bạn không gọi:
future.get();
failure có thể không được xử lý theo cách bạn mong đợi.
Các lựa chọn:
- kiểm tra
Future; - xử lý exception trong
CompletableFuture; - wrapper có logging;
- dùng
execute()khi semantics phù hợp.
Đừng giả định rằng:
task throws
→ chắc chắn có log
11.6 Một Shared Pool Cho Mọi Thứ
Giả sử một pool xử lý:
- request nhanh;
- background jobs;
- slow external API calls.
All tasks
↓
Shared Pool
↓
Slow tasks occupy workers
↓
Fast tasks wait behind them
Một task đáng lẽ mất 5 ms có thể mất hàng giây chỉ vì đứng sau một dependency chậm.
Cách tốt hơn:
Fast workload → Pool A
Slow external calls → Pool B
Đây là bulkhead isolation.
Đừng để một dependency chậm có khả năng chiếm toàn bộ execution capacity của hệ thống.
12. Virtual Threads: Làm Cho Blocking Rẻ Hơn
Platform thread có chi phí vì nó gắn với OS thread.
Virtual thread thay đổi điều đó.
Mô hình:
Platform Thread
Java Thread ↔ OS Thread
Trong khi virtual thread:
Many Virtual Threads
↓
Scheduled on fewer Carrier Threads
↓
OS Threads
Khi virtual thread block ở một blocking operation phù hợp:
Virtual Thread
↓
I/O blocks
↓
Virtual Thread is unmounted
↓
Carrier becomes available
↓
Another Virtual Thread runs
Khi I/O hoàn thành:
Virtual Thread
↓
Scheduled again
↓
Mounted on a carrier
↓
Continue
Ý tưởng chính:
Virtual Threads không làm CPU nhanh hơn. Chúng làm việc chờ đợi rẻ hơn.
12.1 Tạo Virtual Thread
Thread.startVirtualThread(() -> {
// task
});
Hoặc:
Thread.Builder builder = Thread.ofVirtual();
Hoặc:
try (ExecutorService executor =
Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(task);
}
newVirtualThreadPerTaskExecutor() tạo một virtual thread cho mỗi task.
Mô hình tư duy mới cho I/O:
Platform threads:
limited execution slots
→ pool them
Virtual threads:
cheap task carriers
→ one task per thread
12.2 Virtual Thread vs Platform Thread
Với nhiều task chủ yếu sleep() hoặc chờ I/O:
Platform thread pool
→ limited workers
→ remaining tasks wait in queue
Virtual threads
→ one thread per task
→ tasks can all wait concurrently
Do đó Virtual Threads đặc biệt mạnh với:
- HTTP calls;
- database access;
- blocking network I/O;
- file I/O;
- RPC;
- request-per-task workloads.
13. Virtual Threads Không Làm CPU-bound Nhanh Hơn
Đây là hiểu lầm phổ biến.
Virtual thread vẫn cần CPU để chạy.
Nếu máy có 12 core:
12 CPU-bound tasks
→ can use ~12 cores
1,000 CPU-bound virtual threads
→ still share those same cores
Virtual Threads không tạo thêm parallelism.
Với CPU-bound workload, một fixed-size platform-thread executor gần số core thường phù hợp hơn.
Quy tắc:
I/O-bound
→ Virtual Threads are often a strong default
CPU-bound
→ limit parallelism around available cores
14. Virtual Threads Có Xóa Giới Hạn Không?
Không.
Chúng chỉ loại bỏ hoặc giảm thread bottleneck.
Bottleneck vẫn có thể là:
100,000 virtual threads
↓
20 database connections
↓
Only 20 requests can use DB at once
100.000 task vẫn có thể tồn tại.
Nhưng throughput database vẫn bị giới hạn bởi 20 connection.
Vì vậy:
Unlimited threads do not mean unlimited throughput.
Bottleneck chỉ chuyển từ:
Number of threads
sang:
External resource capacity
14.1 Giới hạn concurrency bằng Semaphore
Semaphore semaphore = new Semaphore(20);
semaphore.acquire();
try {
// access limited resource
} finally {
semaphore.release();
}
Mô hình:
1000 virtual tasks
↓
Semaphore(20)
↓
20 access resource
980 wait cheaply
Đây là một kết hợp rất mạnh:
Virtual Threads
→ cheap waiting
Semaphore / connection pool
→ resource protection
Dùng cơ chế phù hợp với tài nguyên:
| Cơ chế | Giới hạn |
|---|---|
Semaphore |
số tác vụ đồng thời |
| Connection pool | DB/socket connections |
| Rate limiter | requests per second |
| Bulkhead | failure isolation |
| Bounded queue | overload buffering |
| Rejection policy | overload response |
15. Producer–Consumer và Backpressure
Khi tạo task trở nên rất rẻ, vấn đề có thể chuyển sang queue và consumer.
Fast Producers
↓
Bounded Queue
↓
Consumers
Nếu producer nhanh hơn consumer:
Without bounds
→ queue grows
With bounds
→ producer must slow down or wait
Backpressure không làm hệ thống nhanh hơn.
Nó ngăn hệ thống tự hủy khi demand lớn hơn capacity.
Một queue bounded nói:
"Tôi chỉ có thể hấp thụ từng này quá tải."
Đó là một property tốt hơn queue tăng vô hạn.
16. Mô Hình Tư Duy Cho Production
Toàn bộ bài viết có thể rút thành một câu:
Thread là một tài nguyên; nhưng bottleneck thực sự thường là thứ thread đang chờ.
Khi thiết kế concurrency, đi theo thứ tự:
Bước 1: Xác định workload
CPU-bound?
I/O-bound?
Mixed?
Bước 2: Tìm bottleneck
CPU?
DB connections?
External API?
Lock?
Memory?
Queue?
Bước 3: Chọn execution model
CPU-bound
→ limit parallelism around cores
I/O-bound
→ consider Virtual Threads
Bước 4: Bảo vệ shared state
Simple atomic state
→ Atomic*
Visibility flag
→ volatile
Complex invariant
→ synchronized / Lock
Bước 5: Bảo vệ external resources
Semaphore
Connection pool
Rate limit
Timeout
Bước 6: Chuẩn bị cho overload
Bounded queue
Rejection policy
Backpressure
Bulkhead isolation
Bước 7: Chuẩn bị cho failure
Timeout
Interrupt handling
Exception handling
Graceful shutdown
Thread dumps
17. Decision Checklist
| Câu hỏi | Hướng xử lý |
|---|---|
| Task chủ yếu I/O-bound? | Cân nhắc Virtual Thread mỗi task |
| Task CPU-bound? | Giới hạn parallelism gần số core |
| External resource có capacity hữu hạn? | Semaphore / pool / rate limit |
| Có thể overload? | Bounded queue + rejection/backpressure |
| Có shared mutable state? | Atomic / synchronized / Lock |
| Có nhiều lock? | Thống nhất lock ordering |
| Dùng ThreadLocal trong pool? | remove() trong finally |
| Có external call? | Luôn đặt timeout |
| Workload latency khác nhau? | Tách executor/bulkhead |
| Pool có vòng đời riêng? | Shutdown đúng cách |
Kết Luận
Java concurrency không bắt đầu bằng:
"Tôi nên tạo bao nhiêu thread?"
Câu hỏi đúng hơn là:
"Task của tôi đang chờ cái gì, và tài nguyên nào thực sự giới hạn hệ thống?"
Platform threads là tài nguyên tương đối đắt, nên thread pool tồn tại để quản lý chúng. Nhưng pool mang theo queue, rejection, sizing, exhaustion và tail latency.
Virtual Threads thay đổi cuộc chơi bằng cách làm blocking rẻ hơn. Với I/O-bound workloads, bạn có thể chuyển sang mô hình:
One task
↓
One virtual thread
Nhưng Virtual Threads không tạo thêm CPU core, không tăng số database connection và không bỏ qua rate limit.
Do đó mô hình cuối cùng là:
Your Code
│
├── CPU-bound
│ → limited parallelism
│
├── I/O-bound
│ → Virtual Threads
│
├── Shared state
│ → Atomic / synchronized / Lock
│
├── External resources
│ → Semaphore / connection pool / rate limit
│
└── Overload
→ bounded queues / backpressure / rejection
Điều quan trọng nhất cần nhớ:
Đừng bắt đầu bằng việc chọn số thread. Hãy bắt đầu bằng bottleneck.
Thread chỉ là cách công việc được thực thi hoặc chờ đợi. Giới hạn thật sự của hệ thống thường nằm ở CPU, lock, queue hoặc một tài nguyên bên ngoài mà các thread đang tranh chấp.
All rights reserved