Họ đang đo gì
Bạn có theo kịp Java hiện tại không, và có phân biệt được tác vụ chặn I/O với tác vụ nặng CPU.
Trả lời ngắn~30 giây
Virtual thread (JEP 444, chính thức từ Java 21) là luồng do JVM quản lý, rẻ tới mức tạo hàng triệu cái được. Khi nó chặn ở I/O, JVM tháo nó khỏi luồng hệ điều hành thay vì chiếm chỗ — nên mô hình “một luồng một request” viết dễ đọc lại chạy được ở quy mô mà trước đây phải dùng reactive. Nó KHÔNG giúp gì cho tác vụ nặng CPU: bạn vẫn chỉ có bấy nhiêu lõi.
Giải thích sâu
Thay đổi lớn nhất về tư duy là pool luồng gần như không còn cần thiết. Trước đây pool tồn tại vì luồng hệ điều hành đắt — mỗi cái tốn khoảng 1MB stack và một lần chuyển ngữ cảnh của nhân. Virtual thread rẻ, nên tạo mới cho từng tác vụ là đúng; giới hạn đồng thời thì đặt bằng semaphore ở đúng chỗ cần giới hạn (ví dụ số kết nối tới một dịch vụ), chứ không phải bằng kích thước pool.
Cái bẫy đáng nói nhất là pinning. Khi virtual thread chặn bên trong khối synchronized, ở Java 21 nó bị ghim vào luồng mang và không tháo ra được — nghĩa là bạn mất đúng lợi ích vừa nói tới, và nếu đủ nhiều luồng bị ghim thì ứng dụng đứng. Cách xử lý là đổi synchronized sang ReentrantLock ở những chỗ có chặn I/O. JEP 491 (Java 24) đã gỡ giới hạn này, nên câu trả lời đầy đủ phải kèm phiên bản.
Cái bẫy thứ hai là ThreadLocal. Nó vẫn hoạt động, nhưng với hàng triệu luồng thì mỗi giá trị lưu trong ThreadLocal nhân lên hàng triệu lần — và những thư viện dùng ThreadLocal làm cache (rất phổ biến, ví dụ SimpleDateFormat được bọc lại) bỗng trở thành vấn đề bộ nhớ. ScopedValue là thứ thay thế được thiết kế cho mô hình mới.
Điều cuối cùng, và là chỗ nhiều người kỳ vọng sai: virtual thread không làm hệ thống của bạn chịu tải cao hơn nếu nút thắt nằm ở database. Bạn chỉ chuyển hàng đợi từ chỗ “chờ luồng” sang chỗ “chờ kết nối database”. Nói được điều này cho thấy bạn nhìn cả hệ thống chứ không chỉ nhìn tầng ứng dụng.
// Một virtual thread cho mỗi tác vụ — không cần pool
try (var scope = Executors.newVirtualThreadPerTaskExecutor()) {
for (var id : orderIds) scope.submit(() -> process(id));
}
// Giới hạn đồng thời đặt ở chỗ có giới hạn thật
private final Semaphore downstream = new Semaphore(20);
downstream.acquire();
try { callPaymentApi(); } finally { downstream.release(); }
// Java 21: chặn trong synchronized -> pinning. Dùng khoá tường minh.
private final ReentrantLock lock = new ReentrantLock();Câu hỏi tiếp theo họ sẽ hỏi
?Vậy còn cần reactive (WebFlux, Reactor) nữa không?
Ít hơn nhiều, cho phần lớn dịch vụ CRUD. Nhưng reactive vẫn hơn ở chỗ backpressure và xử lý luồng dữ liệu — thứ virtual thread không cung cấp. Nếu bài toán của bạn là “nhiều request chặn ở I/O” thì virtual thread thắng về khả năng đọc hiểu; nếu là “luồng sự kiện cần điều tiết” thì reactive vẫn đúng chỗ.
?Structured concurrency giải quyết gì?
Nó buộc các tác vụ con có vòng đời nằm gọn trong một phạm vi: cha chờ hết con, một con lỗi thì huỷ các con còn lại, và stack trace giữ được quan hệ cha–con. Nó vẫn ở dạng preview qua nhiều bản (JEP 453 → 505), nên đáng nhắc tới nhưng chưa nên dựa vào trong production.
Trả lời thế này là mất điểm
- Nói virtual thread làm mọi thứ nhanh hơn. Nó chỉ làm CHỜ rẻ hơn, không làm TÍNH nhanh hơn.
- Không biết tới pinning. Đây là câu hỏi tiếp theo gần như chắc chắn nếu bạn nhắc tới virtual thread.