Chuyển tới nội dung chính

Virtual Threads

Mục tiêu bài học

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

AspectPlatform ThreadsVirtual Threads
Memory~2MB/thread~1KB/thread
Max threadsVài nghìnHàng triệu
SchedulerOSJVM
BlockingBlock OS threadSuspend, release carrier
Context switchExpensive (~1-10µs)Cheap (~1-100ns)
Use caseCPU-boundI/O-bound
Thread poolRequiredNot 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:

  1. Virtual thread unmounts từ carrier thread
  2. Carrier thread free để chạy virtual thread khác
  3. 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:

PropertyDefaultMô tả
jdk.virtualThreadScheduler.parallelismCPU coresSố carrier threads
jdk.virtualThreadScheduler.maxPoolSize256Giới hạn trên (khi có pinning)
jdk.virtualThreadScheduler.minRunnable1Số 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:

  1. Submit: Virtual threads ở trạng thái RUNNABLE được đưa vào run queue
  2. Work Stealing: ForkJoinPool dùng work-stealing algorithm để phân phối virtual threads lên carrier threads
  3. Mount: Runnable virtual thread được gán (mount) lên một carrier thread trống
  4. Execute: Carrier thread chạy code của virtual thread trên CPU
  5. Unmount: Khi gặp blocking I/O, virtual thread unmount khỏi carrier (stack → heap), carrier thread freed
  6. 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!
Khi nào virtual threads shine

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ểmThreadLocalScopedValue
MutabilityMutable (set/get bất kỳ lúc nào)Immutable trong scope
LifetimeKhông giới hạn (dễ leak)Bounded — tự cleanup
InheritanceInheritableThreadLocal (copy)Tự động share (zero-copy)
MemoryMỗi thread 1 copyShare 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íReactiveVirtual Threads
Learning curveRất caoThấp (giống sequential code)
DebuggingKhó (async stack traces)Dễ (sequential stack traces)
BackpressureBuilt-inPhải tự implement
CPU-boundTốt (schedulers)Không phù hợp
I/O-boundTốtRất tốt
EcosystemMature (WebFlux, R2DBC)Growing (JDBC, blocking APIs)
Code readabilityThấ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 đầu
  • jdk.VirtualThreadEnd — virtual thread kết thúc
  • jdk.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ẹ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();
}
}
OCP Trap — synchronized gây pinning

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.

OCP Trap — Đừng pool virtual threads

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).

📖 Theo JEP 444 — Virtual Threads
  • Virtual threads là Thread instances — KHÔNG phải subclass mới. Tương thích hoàn toàn với existing Thread API
  • Thread.isVirtual() trả về true cho virtual threads
  • Virtual threads luôn là daemon threadssetDaemon(false) throw IllegalArgumentException
  • Virtual threads không có thread priority — setPriority() bị ignore
  • Carrier thread pool mặc định: ForkJoinPool vớ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

ApproachComplexityPerformanceScalability
Platform threadsLowLow (I/O-bound)Low
Thread poolsMediumMediumMedium
Reactive (Reactor)HighHighHigh
Virtual threadsLowHighVery 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
Migration strategy
  1. Java 21+: Enable virtual threads trong Spring Boot
  2. Test thoroughly: Đặc biệt blocking operations
  3. Monitor pinning: Dùng JVM flags
  4. Replace synchronized: Với ReentrantLock nếu cần
  5. 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:

  1. Fetch từ DB
  2. Call external API
  3. Transform data
  4. Save lại DB

Implement với virtual threads và measure performance.

Tài liệu tham khảo

Đọc thêm