Virtual Threads
Sau bài này, bạn sẽ:
- Hiểu Virtual Threads (Java 21) — lightweight threads cho high-concurrency I/O-bound applications
- Phân biệt platform threads vs virtual threads (memory, scheduling, use case)
- Biết cách tạo virtual threads với Thread.ofVirtual() và Executors.newVirtualThreadPerTaskExecutor()
- Nắm được Structured Concurrency (preview) và scoped values
- Áp dụng virtual threads cho thread-per-request model và tránh pinning issues
Bài trước: Pattern Matching — Đã học về pattern matching for instanceof và switch. Bài này sẽ tìm hiểu Virtual Threads — bước đột phá concurrency trong Java 21.
Giới thiệu
Virtual threads là một trong những tính năng quan trọng nhất của Java 21, được phát triển trong Project Loom. Virtual threads là lightweight threads cho phép viết concurrent code theo thread-per-request model mà không lo về performance overhead.
Virtual threads được giới thiệu như preview feature trong Java 19-20, và trở thành standard feature trong Java 21 (September 2023).
Vấn đề Virtual Threads giải quyết
Thread-per-request Model
Trước đây, Java web applications thường dùng thread pool với fixed số lượng threads:
// Traditional approach - Thread pool
ExecutorService executor = Executors.newFixedThreadPool(200);
// Handle 10,000 concurrent requests
// Nhưng chỉ có 200 threads -> Chờ đợi, blocking
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> handleRequest());
}
Vấn đề:
- Platform threads đắt: Mỗi thread = 1 OS thread, tốn ~2MB stack memory
- Limited scalability: Chỉ tạo được vài nghìn threads
- Thread blocking: Khi thread chờ I/O, nó không làm gì nhưng vẫn chiếm tài nguyên
Tại sao không tạo nhiều threads hơn?
// Nếu tạo 100,000 platform threads
// 100,000 threads × 2MB = 200GB RAM!
// JVM và OS không handle được
Reactive Programming (Alternative)
// Reactive approach (Project Reactor, RxJava)
Mono.fromCallable(() -> fetchUser(id))
.flatMap(user -> fetchOrders(user.getId()))
.flatMap(orders -> processOrders(orders))
.subscribe(result -> sendResponse(result));
Vấn đề:
- Khó học: Steep learning curve
- Khó debug: Stack traces phức tạp
- Khó maintain: Code không sequential
Virtual Threads: Best of Both Worlds
// Virtual threads - Simple + Scalable!
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Tạo 1 TRIỆU virtual threads - KHÔNG VẤN ĐỀ!
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> handleRequest());
}
}
Platform Threads vs Virtual Threads
Platform Threads (Traditional)
// Platform thread = OS thread wrapper
Thread platformThread = new Thread(() -> {
System.out.println("Running on platform thread");
});
platformThread.start();
Đặc điểm:
- 1:1 mapping với OS threads
- Tốn ~2MB stack memory/thread
- Managed bởi OS scheduler
- Heavyweight, giới hạn số lượng
Virtual Threads (Java 21)
// Virtual thread = managed by JVM
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Running on virtual thread");
});
Đặc điểm:
- M:N mapping - nhiều virtual threads trên ít platform threads
- Lightweight - chỉ vài KB/thread
- Managed bởi JVM scheduler
- Có thể tạo hàng triệu threads
So sánh chi tiết
| Aspect | Platform Threads | Virtual Threads |
|---|---|---|
| Memory | ~2MB/thread | ~1KB/thread |
| Max threads | Vài nghìn | Hàng triệu |
| Scheduler | OS | JVM |
| Blocking | Block OS thread | Suspend, release carrier |
| Context switch | Expensive (~1-10µs) | Cheap (~1-100ns) |
| Use case | CPU-bound | I/O-bound |
| Thread pool | Required | Not needed |
Platform Threads: Mỗi Java thread = 1 OS thread (1:1). Tạo 10,000 threads = 10,000 OS threads = ~20GB RAM. Virtual Threads: Hàng triệu virtual threads chia sẻ vài carrier threads (M:N). JVM tự mount/unmount.
Tạo Virtual Threads
Method 1: Thread.ofVirtual()
// Factory method
Thread thread = Thread.ofVirtual()
.name("virtual-worker")
.start(() -> {
System.out.println("Task running on: " +
Thread.currentThread());
});
// Unstarted virtual thread
Thread unstarted = Thread.ofVirtual()
.unstarted(() -> System.out.println("Not started yet"));
unstarted.start(); // Start manually
Method 2: Thread.startVirtualThread()
// Convenience method - create + start
Thread thread = Thread.startVirtualThread(() -> {
System.out.println("Running in virtual thread");
});
Method 3: Executors.newVirtualThreadPerTaskExecutor()
// ExecutorService với virtual threads
try (ExecutorService executor =
Executors.newVirtualThreadPerTaskExecutor()) {
// Submit tasks - mỗi task = 1 virtual thread
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Result";
});
String result = future.get();
}
Method 4: Thread.Builder
// Thread builder cho nhiều threads
ThreadFactory factory = Thread.ofVirtual()
.name("worker-", 0) // worker-0, worker-1, ...
.factory();
Thread t1 = factory.newThread(() -> task1());
Thread t2 = factory.newThread(() -> task2());
t1.start();
t2.start();
Cách Virtual Threads hoạt động
Carrier Threads
Virtual threads chạy trên carrier threads (platform threads):
[Virtual Thread 1]
[Virtual Thread 2] → [Carrier Thread 1] (Platform)
[Virtual Thread 3] → [Carrier Thread 2] (Platform)
[Virtual Thread 4]
Khi virtual thread blocks:
- Virtual thread unmounts từ carrier thread
- Carrier thread free để chạy virtual thread khác
- Khi I/O complete, virtual thread mounts lại
// Virtual thread blocking
Thread.startVirtualThread(() -> {
// 1. Mount lên carrier thread
System.out.println("Started");
// 2. Blocking I/O - unmount, carrier thread freed
String data = readFromNetwork();
// 3. I/O done - mount lại (có thể khác carrier thread)
System.out.println("Finished");
});
Mount/Unmount Lifecycle
Điểm mấu chốt: Khi virtual thread gặp blocking I/O, nó tự động unmount khỏi carrier thread. Stack frame được lưu vào heap memory (rất nhẹ ~vài KB). Carrier thread được giải phóng để chạy virtual thread khác. Đây là lý do virtual threads scale tốt hơn platform threads rất nhiều.
Work Stealing
JVM dùng ForkJoinPool làm carrier thread pool với work-stealing algorithm:
// Default carrier pool size = số CPU cores
int carriers = Runtime.getRuntime().availableProcessors();
// Override với system property
// -Djdk.virtualThreadScheduler.parallelism=10
Chi tiết Carrier Thread Pool
JVM dùng ForkJoinPool đặc biệt làm scheduler cho virtual threads. Một số system properties quan trọng:
| Property | Default | Mô tả |
|---|---|---|
jdk.virtualThreadScheduler.parallelism | CPU cores | Số carrier threads |
jdk.virtualThreadScheduler.maxPoolSize | 256 | Giới hạn trên (khi có pinning) |
jdk.virtualThreadScheduler.minRunnable | 1 | Số thread tối thiểu không bị block |
// Kiểm tra carrier thread pool
Thread.startVirtualThread(() -> {
// Lấy thông tin carrier thread
System.out.println(Thread.currentThread());
// VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1
// ↑ carrier thread name
});
Lưu ý: Carrier thread pool là internal implementation — không nên phụ thuộc vào chi tiết này. JVM có thể thay đổi scheduler trong tương lai.
Mô hình Scheduling: Virtual Threads trên Carrier Threads
Sơ đồ dưới đây minh họa cách JVM scheduler (ForkJoinPool) multiplexes hàng triệu virtual threads lên một số ít carrier threads (platform threads):
Các bước scheduling:
- Submit: Virtual threads ở trạng thái RUNNABLE được đưa vào run queue
- Work Stealing: ForkJoinPool dùng work-stealing algorithm để phân phối virtual threads lên carrier threads
- Mount: Runnable virtual thread được gán (mount) lên một carrier thread trống
- Execute: Carrier thread chạy code của virtual thread trên CPU
- Unmount: Khi gặp blocking I/O, virtual thread unmount khỏi carrier (stack → heap), carrier thread freed
- Remount: Khi I/O complete, virtual thread được đưa lại vào run queue để mount lại
Work Stealing: Nếu một carrier thread hết việc, nó sẽ "steal" virtual threads từ queue của carrier thread khác. Điều này đảm bảo load balancing và CPU utilization cao.
Performance: Virtual Threads shine
Ví dụ: HTTP Server
// Traditional platform threads - Limited concurrency
public class PlatformThreadServer {
public static void main(String[] args) throws Exception {
var executor = Executors.newFixedThreadPool(200);
var server = HttpServer.create(
new InetSocketAddress(8080), 0
);
server.createContext("/", exchange -> {
executor.submit(() -> handleRequest(exchange));
});
server.start();
// Max 200 concurrent requests
}
}
// Virtual threads - UNLIMITED concurrency!
public class VirtualThreadServer {
public static void main(String[] args) throws Exception {
var executor = Executors.newVirtualThreadPerTaskExecutor();
var server = HttpServer.create(
new InetSocketAddress(8080), 0
);
server.createContext("/", exchange -> {
executor.submit(() -> handleRequest(exchange));
});
server.start();
// Handle MILLIONS concurrent requests!
}
private static void handleRequest(HttpExchange exchange) {
try {
// Simulated I/O operations
Thread.sleep(100); // Database query
Thread.sleep(50); // External API call
Thread.sleep(30); // Cache lookup
String response = "Hello, World!";
exchange.sendResponseHeaders(200, response.length());
exchange.getResponseBody().write(response.getBytes());
} catch (Exception e) {
e.printStackTrace();
} finally {
exchange.close();
}
}
}
Benchmark Results
Platform threads (200 threads):
- Throughput: ~1,000 requests/sec
- Max concurrent: 200
Virtual threads (unlimited):
- Throughput: ~100,000 requests/sec
- Max concurrent: 1,000,000+
- Memory: Comparable to platform threads!
Virtual threads hiệu quả nhất với I/O-bound workloads:
- Network calls (HTTP, database)
- File I/O
- Blocking operations
- High concurrency requirements
Structured Concurrency (Preview)
Structured concurrency giúp quản lý lifecycle của concurrent operations:
// Traditional approach - Messy
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> future1 = executor.submit(() -> fetchUser());
Future<String> future2 = executor.submit(() -> fetchOrders());
// Phải manually handle shutdown, exceptions, cancellation...
// Structured concurrency - Clean!
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser());
Future<Order> orders = scope.fork(() -> fetchOrders());
scope.join(); // Wait for all tasks
scope.throwIfFailed(); // Propagate exceptions
// Both succeeded
processData(user.resultNow(), orders.resultNow());
}
// Auto cleanup when scope closes
Shutdown policies
// ShutdownOnFailure - Stop all nếu 1 task fails
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var task1 = scope.fork(() -> operation1());
var task2 = scope.fork(() -> operation2());
// Nếu task1 fails -> task2 bị cancel
scope.join().throwIfFailed();
}
// ShutdownOnSuccess - Stop all khi 1 task succeeds
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> fetchFromPrimaryDB());
scope.fork(() -> fetchFromSecondaryDB());
scope.fork(() -> fetchFromCache());
scope.join();
String result = scope.result(); // First successful result
}
Virtual Thread Lifecycle
Sơ đồ dưới đây mô tả các trạng thái của virtual thread từ khi được tạo đến khi kết thúc:
Lưu ý: Trạng thái Unmounted là lợi thế lớn nhất của virtual threads — carrier thread được giải phóng khi virtual thread block I/O. Trạng thái Pinned là vấn đề cần tránh vì nó khiến carrier thread bị block.
Khi nào dùng Virtual Threads
✅ GOOD use cases
1. Web servers / HTTP handlers
// Perfect for virtual threads!
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// Blocking calls - no problem với virtual threads
User user = userService.findById(id);
List<Order> orders = orderService.findByUserId(id);
return enrichUser(user, orders);
}
}
// Spring Boot config cho virtual threads (Java 21+)
// application.properties
// spring.threads.virtual.enabled=true
2. Database access
// JDBC với virtual threads - Beautiful!
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<User>> futures = new ArrayList<>();
for (long id : userIds) {
futures.add(executor.submit(() -> {
try (var conn = dataSource.getConnection();
var stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
stmt.setLong(1, id);
var rs = stmt.executeQuery();
return mapToUser(rs);
}
}));
}
// Wait cho tất cả queries
List<User> users = futures.stream()
.map(f -> {
try { return f.get(); }
catch (Exception e) { throw new RuntimeException(e); }
})
.toList();
}
3. Microservices communication
public class OrderService {
public OrderDetails getOrderDetails(Long orderId) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Parallel calls tới multiple services
var orderFuture = scope.fork(() ->
orderClient.getOrder(orderId));
var customerFuture = scope.fork(() ->
customerClient.getCustomer(order.customerId));
var productsFuture = scope.fork(() ->
productClient.getProducts(order.productIds));
scope.join().throwIfFailed();
return new OrderDetails(
orderFuture.resultNow(),
customerFuture.resultNow(),
productsFuture.resultNow()
);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
4. Batch processing
// Process 1 million records - 1 virtual thread per record!
public void processBatchRecords(List<Record> records) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
records.forEach(record -> {
executor.submit(() -> {
processRecord(record);
});
});
}
// All tasks complete khi executor closes
}
❌ BAD use cases
1. CPU-bound tasks
// BAD - CPU intensive work
Thread.startVirtualThread(() -> {
// Compute prime numbers - blocks CPU, không có I/O
for (long i = 0; i < 1_000_000_000; i++) {
isPrime(i);
}
});
// Virtual threads KHÔNG giúp gì cho CPU-bound!
// Dùng platform threads hoặc parallel streams
2. synchronized blocks (Pinning)
// BAD - synchronized pins virtual thread to carrier
Thread.startVirtualThread(() -> {
synchronized (lock) { // Virtual thread PINNED!
blockingIO(); // Carrier thread blocked!
}
});
// GOOD - Dùng ReentrantLock
Thread.startVirtualThread(() -> {
lock.lock();
try {
blockingIO(); // Virtual thread có thể unmount
} finally {
lock.unlock();
}
});
3. ThreadLocal heavy usage
// BAD - Nhiều ThreadLocals với millions virtual threads
ThreadLocal<ExpensiveObject> threadLocal = new ThreadLocal<>();
// Millions virtual threads × ExpensiveObject = OOM!
ScopedValue — Thay thế ThreadLocal
ScopedValue (Preview từ Java 21) là alternative cho ThreadLocal, thiết kế riêng cho virtual threads:
Vấn đề với ThreadLocal
// ThreadLocal + millions virtual threads = Memory disaster
ThreadLocal<UserContext> context = new ThreadLocal<>();
// Mỗi virtual thread = 1 copy → triệu copies!
// ThreadLocal không tự cleanup khi virtual thread kết thúc
ScopedValue: Immutable, bounded
// ScopedValue - Immutable, automatic cleanup
private static final ScopedValue<UserContext> CONTEXT = ScopedValue.newInstance();
void handleRequest(Request req) {
UserContext ctx = authenticate(req);
// Bind value cho scope hiện tại
ScopedValue.runWhere(CONTEXT, ctx, () -> {
// CONTEXT.get() available trong scope này
processRequest();
// Khi scope kết thúc → tự cleanup
});
}
void processRequest() {
UserContext ctx = CONTEXT.get(); // Đọc value
// ...
}
So sánh ThreadLocal vs ScopedValue
| Đặc điểm | ThreadLocal | ScopedValue |
|---|---|---|
| Mutability | Mutable (set/get bất kỳ lúc nào) | Immutable trong scope |
| Lifetime | Không giới hạn (dễ leak) | Bounded — tự cleanup |
| Inheritance | InheritableThreadLocal (copy) | Tự động share (zero-copy) |
| Memory | Mỗi thread 1 copy | Share across child scopes |
| Virtual Threads | ⚠️ Không phù hợp | ✅ Thiết kế riêng |
Khuyến nghị: Với virtual threads, ưu tiên ScopedValue. ThreadLocal chỉ dùng khi cần mutable per-thread state (hiếm khi cần với virtual threads).
So sánh: Reactive vs Virtual Threads
Code complexity
// Reactive (Project Reactor)
Mono<OrderDetails> getOrderDetails(Long orderId) {
return orderRepo.findById(orderId)
.flatMap(order -> Mono.zip(
customerRepo.findById(order.customerId()),
productRepo.findAllById(order.productIds()).collectList()
).map(tuple -> new OrderDetails(order, tuple.getT1(), tuple.getT2())));
}
// Virtual Threads — Sequential, dễ đọc
OrderDetails getOrderDetails(Long orderId) {
Order order = orderRepo.findById(orderId); // blocking OK!
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var customer = scope.fork(() -> customerRepo.findById(order.customerId()));
var products = scope.fork(() -> productRepo.findAllById(order.productIds()));
scope.join().throwIfFailed();
return new OrderDetails(order, customer.resultNow(), products.resultNow());
}
}
Khi nào chọn gì?
| Tiêu chí | Reactive | Virtual Threads |
|---|---|---|
| Learning curve | Rất cao | Thấp (giống sequential code) |
| Debugging | Khó (async stack traces) | Dễ (sequential stack traces) |
| Backpressure | Built-in | Phải tự implement |
| CPU-bound | Tốt (schedulers) | Không phù hợp |
| I/O-bound | Tốt | Rất tốt |
| Ecosystem | Mature (WebFlux, R2DBC) | Growing (JDBC, blocking APIs) |
| Code readability | Thấp (operator chains) | Cao (imperative style) |
Khuyến nghị thực tế: Với project mới dùng Java 21+, ưu tiên virtual threads. Reactive chỉ khi cần backpressure hoặc đã có codebase reactive sẵn.
Migration từ Platform Threads
1. ExecutorService
// Before
ExecutorService executor = Executors.newFixedThreadPool(100);
// After - Just 1 line change!
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
2. Thread creation
// Before
Thread thread = new Thread(() -> task());
thread.start();
// After
Thread thread = Thread.startVirtualThread(() -> task());
3. Spring Boot applications
// application.properties (Spring Boot 3.2+)
spring.threads.virtual.enabled=true
// Hoặc manually
@Configuration
public class AsyncConfig {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(
Executors.newVirtualThreadPerTaskExecutor()
);
}
}
4. CompletableFuture
// Before
CompletableFuture.supplyAsync(() -> task(), platformThreadExecutor);
// After
CompletableFuture.supplyAsync(() -> task(),
Executors.newVirtualThreadPerTaskExecutor());
Pinning Issue
Thread pinning xảy ra khi virtual thread không thể unmount từ carrier thread:
Causes of Pinning
1. synchronized blocks
// PINNED - Virtual thread stuck on carrier
synchronized (monitor) {
blockingOperation(); // Carrier thread blocked!
}
2. Native methods / FFI
// PINNED - Native code execution
nativeMethod(); // JNI call
Detecting Pinning
// JVM flag để detect pinning
// -Djdk.tracePinnedThreads=full
Thread.startVirtualThread(() -> {
synchronized (lock) {
Thread.sleep(1000); // Warning printed!
}
});
Monitoring Pinning với JFR (Java Flight Recorder)
// Bật JFR recording
// java -XX:StartFlightRecording=filename=recording.jfr,settings=profile MyApp
// Hoặc programmatically
import jdk.jfr.*;
try (var recording = new Recording()) {
recording.enable("jdk.VirtualThreadPinned")
.withThreshold(Duration.ofMillis(20));
recording.start();
// ... run application ...
recording.stop();
recording.dump(Path.of("virtual-threads.jfr"));
}
JFR Events quan trọng:
jdk.VirtualThreadStart— virtual thread bắt đầujdk.VirtualThreadEnd— virtual thread kết thúcjdk.VirtualThreadPinned— virtual thread bị pin (⚠️ quan trọng nhất!)jdk.VirtualThreadSubmitFailed— không thể submit virtual thread
# Phân tích JFR recording
jfr print --events jdk.VirtualThreadPinned recording.jfr
Avoiding Pinning
// Solution 1: Replace synchronized với ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
blockingOperation();
} finally {
lock.unlock();
}
// Solution 2: Keep synchronized blocks short
synchronized (lock) {
// Quick operation only
updateCounter();
}
// Blocking I/O outside synchronized
blockingIO();
Best Practices
Virtual Threads Best Practices & Pitfalls
Sơ đồ tư duy dưới đây tổng hợp các best practices, pitfalls, và giải pháp khi làm việc với virtual threads:
Nguyên tắc vàng: Virtual threads khác platform threads ở chỗ chúng rất nhẹ và tạo nhanh. Đừng áp dụng platform thread patterns (pooling, reuse) cho virtual threads — cứ tạo mới cho mỗi task!
1. Don't pool virtual threads
// BAD - Pooling virtual threads
ExecutorService pool = Executors.newFixedThreadPool(1000,
Thread.ofVirtual().factory());
// GOOD - Create on demand
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
2. Avoid ThreadLocal abuse
// BAD - Heavy ThreadLocal với millions threads
ThreadLocal<DatabaseConnection> dbConnection = new ThreadLocal<>();
// GOOD - Pass dependencies explicitly
public void processRequest(DatabaseConnection conn) {
// Use conn parameter
}
3. Use structured concurrency
// GOOD - Structured concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var task1 = scope.fork(() -> operation1());
var task2 = scope.fork(() -> operation2());
scope.join().throwIfFailed();
}
// BAD - Manual thread management
Thread t1 = Thread.startVirtualThread(() -> operation1());
Thread t2 = Thread.startVirtualThread(() -> operation2());
t1.join();
t2.join();
4. Avoid synchronized in virtual threads
// GOOD - ReentrantLock
private final Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
criticalSection();
} finally {
lock.unlock();
}
}
Debugging Virtual Threads
Thread dumps
// Traditional thread dump
jstack <pid>
// Virtual threads có riêng format
// - Carrier threads shown
// - Mounted virtual threads shown
// - Parked virtual threads shown separately
Thread names
// Name virtual threads for debugging
Thread.ofVirtual()
.name("request-handler-", 0)
.factory()
.newThread(() -> handleRequest());
// Stack trace sẽ show: "request-handler-123"
Ví dụ thực tế: Concurrent HTTP Requests
public class ConcurrentHTTPFetcher {
private static final HttpClient client = HttpClient.newHttpClient();
// Fetch 1000 URLs concurrently với virtual threads
public static void main(String[] args) throws Exception {
List<String> urls = generateUrls(1000);
long start = System.currentTimeMillis();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = urls.stream()
.map(url -> executor.submit(() -> fetchUrl(url)))
.toList();
// Wait for all
List<String> results = futures.stream()
.map(f -> {
try { return f.get(); }
catch (Exception e) { return "Error: " + e.getMessage(); }
})
.toList();
long duration = System.currentTimeMillis() - start;
System.out.println("Fetched " + results.size() +
" URLs in " + duration + "ms");
// With virtual threads: ~1-2 seconds
// With platform threads (100 pool): ~10-20 seconds
}
}
private static String fetchUrl(String url) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
return response.body();
}
private static List<String> generateUrls(int count) {
return IntStream.range(0, count)
.mapToObj(i -> "https://example.com/api/" + i)
.toList();
}
}
synchronized block/method khiến virtual thread không thể unmount khỏi carrier thread. Carrier thread bị block → giảm throughput:
// ❌ synchronized pins virtual thread
synchronized (lock) {
Thread.sleep(1000); // Carrier thread bị block 1 giây!
}
// ✅ ReentrantLock — virtual thread unmount bình thường
lock.lock();
try {
Thread.sleep(1000); // Virtual thread unmount, carrier freed
} finally {
lock.unlock();
}
Exam có thể hỏi: "Phương pháp nào giảm pinning?" → Trả lời: ReentrantLock thay thế synchronized.
Pooling virtual threads là anti-pattern — phá vỡ lợi thế chính (lightweight, create-on-demand):
// ❌ ANTI-PATTERN — pool virtual threads
ExecutorService pool = Executors.newFixedThreadPool(100,
Thread.ofVirtual().factory());
// Giới hạn 100 virtual threads → mất ý nghĩa!
// ✅ Đúng cách — mỗi task 1 virtual thread
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// Tạo virtual thread cho MỖI task — không giới hạn
Virtual threads rất nhẹ (~1KB), tạo mới nhanh hơn lấy từ pool. Pool chỉ có ý nghĩa với platform threads (đắt ~2MB).
- Virtual threads là
Threadinstances — KHÔNG phải subclass mới. Tương thích hoàn toàn với existing Thread API Thread.isVirtual()trả vềtruecho virtual threads- Virtual threads luôn là daemon threads —
setDaemon(false)throwIllegalArgumentException - Virtual threads không có thread priority —
setPriority()bị ignore - Carrier thread pool mặc định:
ForkJoinPoolvới parallelism = CPU cores - Blocking operations (I/O,
Thread.sleep(),Lock.lock()) tự động unmount virtual thread
Tham khảo: JEP 444 | Oracle Virtual Threads Guide
Kết luận
Virtual Threads vs Alternatives
| Approach | Complexity | Performance | Scalability |
|---|---|---|---|
| Platform threads | Low | Low (I/O-bound) | Low |
| Thread pools | Medium | Medium | Medium |
| Reactive (Reactor) | High | High | High |
| Virtual threads | Low | High | Very High |
Lựa chọn loại thread phù hợp
Sơ đồ quyết định dưới đây giúp bạn chọn loại thread/concurrency model phù hợp với use case:
Decision guide: Virtual threads là lựa chọn tốt nhất cho I/O-bound workloads với Java 21+. Chỉ cần kiểm tra và thay thế synchronized blocks bằng ReentrantLock để tránh pinning.
Khuyến nghị
✅ Dùng Virtual Threads cho:
- I/O-bound applications
- High concurrency requirements
- Simple, readable code
- Blocking APIs (JDBC, HTTP)
❌ Không dùng cho:
- CPU-bound tasks
- Code có nhiều synchronized
- Heavy ThreadLocal usage
- Java 21+: Enable virtual threads trong Spring Boot
- Test thoroughly: Đặc biệt blocking operations
- Monitor pinning: Dùng JVM flags
- Replace synchronized: Với ReentrantLock nếu cần
- Measure: So sánh performance trước/sau
Bài tập thực hành
Bài 1: Simple Virtual Threads
Tạo 10,000 virtual threads, mỗi thread sleep 1 second rồi print thread name. So sánh với platform threads.
Bài 2: HTTP API Aggregator
Viết service fetch data từ 5 external APIs concurrently sử dụng virtual threads và structured concurrency. Nếu 1 API fails, cancel tất cả.
Bài 3: Batch Processing
Process 100,000 records từ database, mỗi record cần:
- Fetch từ DB
- Call external API
- Transform data
- Save lại DB
Implement với virtual threads và measure performance.
Tài liệu tham khảo
- JEP 444: Virtual Threads
- JEP 453: Structured Concurrency (Preview)
- Oracle Virtual Threads Guide
- Project Loom