Virtual thread chạy theo mô hình M:N: nhiều virtual thread được gắn (mount) lên một số ít carrier thread là platform thread. Khi virtual thread chặn ở I/O, nó gỡ (unmount) khỏi carrier để carrier phục vụ virtual thread khác — đó là toàn bộ lợi ích về khả năng mở rộng.
Pinning là tình huống virtual thread không gỡ ra được, giữ chặt carrier trong suốt thời gian chặn. Trong Java 21 (JEP 444), pinning xảy ra khi:
- Đang ở trong khối/method synchronized — monitor được ghi nhận theo carrier thread.
- Đang trong lời gọi native / JNI.
Một carrier bị giữ mà chờ I/O 2 giây thì mọi virtual thread khác đợi carrier đó cũng đứng im; carrier pool mặc định chỉ bằng số CPU nên vài chỗ pinning là đủ làm nghẽn. Bật -Djdk.tracePinnedThreads=full để in stack trace mỗi lần pin.
Cách xử lý: thay synchronized bao quanh đoạn có I/O bằng ReentrantLock — ReentrantLock xây trên LockSupport.park() nên gỡ mount bình thường. Từ Java 24, JEP 491 đã sửa synchronized để không còn pin carrier nữa; nhưng khi phỏng vấn với runtime 21 thì đây vẫn là điểm phải biết.
Không pool virtual thread vì pool tồn tại để tái dùng tài nguyên đắt. Virtual thread rẻ (vài trăm byte, tạo hàng triệu được) nên gộp chúng vào pool cố định chỉ dựng lại đúng cái trần đồng thời mà virtual thread sinh ra để gỡ bỏ.
// đúng: mỗi task một virtual thread mới
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
tasks.forEach(executor::submit);
}Muốn giới hạn tải xuống hệ thống hạ nguồn thì dùng Semaphore để chặn số lượng đồng thời, không dùng pool thread.
Cũng lưu ý: ThreadLocal vẫn hoạt động nhưng khi có hàng triệu virtual thread, mỗi cái một bản sao giá trị — cân nhắc kỹ dung lượng.