# Spring Boot 3.4 Virtual Threads: Câu Hỏi Phỏng Vấn và Benchmark Hiệu Năng > Làm chủ Virtual Threads của Java 21 với Spring Boot 3.4: 15 câu hỏi phỏng vấn, benchmark hiệu năng và mẫu di chuyển để vượt qua các buổi phỏng vấn kỹ thuật. - Published: 2026-03-15 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, virtual threads, java 21, project loom, performance - Reading time: 16 min --- Virtual Threads là một trong những cải tiến quan trọng nhất của Java 21, và Spring Boot 3.4 hỗ trợ chúng một cách native. Tính năng thuộc Project Loom này thay đổi cách xử lý concurrency trong các ứng dụng backend. Các buổi phỏng vấn kỹ thuật ngày nay đánh giá việc nắm vững cơ chế bên trong, các tình huống sử dụng phù hợp và những bẫy phổ biến cần tránh. > **Mẹo Chuẩn Bị** > > Người phỏng vấn phân biệt rõ giữa ứng viên thực sự hiểu Virtual Threads và những người dùng chúng một cách máy móc. Biết khi nào KHÔNG nên dùng quan trọng không kém việc biết lợi ích của chúng. ## Nền Tảng Virtual Threads ### Câu hỏi 1: Virtual Thread là gì và khác Platform Thread ra sao? Virtual Thread là một thread nhẹ do JVM quản lý chứ không phải hệ điều hành. Khác với Platform Threads (thread truyền thống), Virtual Threads không ánh xạ trực tiếp tới thread của OS. JVM có thể tạo ra hàng triệu Virtual Thread với mức tiêu hao bộ nhớ tối thiểu. ```java // VirtualThreadDemo.java // Comparing thread creation approaches public class VirtualThreadDemo { public void demonstrateDifference() { // Platform Thread: ~1MB stack per thread // Practical limit: a few thousand on a standard JVM Thread platformThread = new Thread(() -> { performBlockingOperation(); }); // Virtual Thread: ~a few KB per thread // Can create millions without issues Thread virtualThread = Thread.ofVirtual().start(() -> { performBlockingOperation(); }); } // A Virtual Thread "mounts" onto a Platform Thread (carrier) // During I/O blocking, it releases the carrier for other Virtual Threads private void performBlockingOperation() { try { Thread.sleep(1000); // Virtual Thread detaches from carrier here } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } ``` Khác biệt cốt lõi nằm ở hành vi khi blocking. Khi Platform Thread bị blocking (I/O, sleep), nó vẫn dính chặt vào thread của OS. Trong khi đó Virtual Thread "unmount" khỏi carrier thread, giải phóng carrier cho các Virtual Thread khác. ### Câu hỏi 2: Cách bật Virtual Threads trong Spring Boot 3.4? Spring Boot 3.4 thu gọn việc bật Virtual Threads xuống còn một thuộc tính cấu hình duy nhất. Toàn bộ framework tự động thích ứng: Tomcat, REST controller và các lời gọi blocking lập tức hưởng lợi từ tối ưu này. ```yaml # application.yml # Global Virtual Threads activation spring: threads: virtual: enabled: true # Optional Tomcat pool configuration server: tomcat: threads: max: 200 # Less critical with Virtual Threads min-spare: 10 ``` ```java // WebConfig.java // Programmatic activation if needed @Configuration public class WebConfig { @Bean public TomcatProtocolHandlerCustomizer virtualThreadCustomizer() { // Each HTTP request runs on a Virtual Thread return protocolHandler -> { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; } } ``` Việc bật Virtual Threads thay đổi hành vi của Tomcat: thay vì pool thread cố định, mỗi request nhận một Virtual Thread riêng. Cách tiếp cận này loại bỏ nút thắt cổ chai cố hữu của thread pool. ### Câu hỏi 3: Giải thích khái niệm "mounting" và "unmounting" Mounting là việc gắn Virtual Thread vào carrier thread (Platform Thread). Unmounting xảy ra trong các thao tác blocking và giải phóng carrier cho Virtual Thread khác. Cơ chế này cho phép tận dụng tối đa tài nguyên CPU. ```java // MountingDemo.java // Illustrating the mounting/unmounting cycle public class MountingDemo { public void demonstrateMounting() { Thread.ofVirtual().start(() -> { // MOUNTED: Virtual Thread is using a carrier thread System.out.println("Carrier: " + getCurrentCarrier()); // UNMOUNTING: releases carrier during blocking performDatabaseQuery(); // Blocking JDBC call // REMOUNTED: may be on a different carrier System.out.println("New carrier: " + getCurrentCarrier()); }); } // Blocking operations trigger unmounting automatically private void performDatabaseQuery() { // JDBC connection, file read, network call... // Virtual Thread detaches during I/O wait } private String getCurrentCarrier() { // Gets the current carrier thread name return Thread.currentThread().toString(); } } ``` Cơ chế này hoàn toàn trong suốt với lập trình viên. Code vẫn được viết theo phong cách imperative cổ điển, song JVM tự tối ưu việc dùng carrier. Một pool nhỏ vài carrier có thể phục vụ hàng triệu Virtual Thread. > **Ghi Chú Kỹ Thuật** > > Số lượng carrier thread mặc định bằng số core CPU. JVM điều chỉnh pool này một cách động thông qua ForkJoinPool. ## Tình Huống Sử Dụng và Anti-pattern ### Câu hỏi 4: Khi nào Virtual Threads mang lại cải thiện hiệu năng? Virtual Threads tỏa sáng với khối lượng công việc I/O-bound: gọi REST bên ngoài, truy vấn database, đọc file. Những thao tác này dành phần lớn thời gian để chờ, và trong khoảng thời gian đó Virtual Thread giải phóng carrier của mình. ```java // IOBoundService.java // Ideal case for Virtual Threads @Service public class IOBoundService { private final RestClient restClient; private final UserRepository userRepository; // Each call involves network and database waiting // Virtual Threads shine here public UserProfile enrichUserProfile(Long userId) { // DB call - VT detaches during SQL query User user = userRepository.findById(userId).orElseThrow(); // External REST call - VT detaches during HTTP wait ExternalData externalData = restClient .get() .uri("/api/external/{id}", userId) .retrieve() .body(ExternalData.class); // Data aggregation return new UserProfile(user, externalData); } } ``` Lợi ích đến từ việc xử lý số lượng request đồng thời lớn hơn nhiều. Với 200 Platform Threads và request 100ms, throughput tối đa là 2.000 req/s. Với Virtual Threads, con số này có thể vượt 50.000 req/s trên cùng một máy. ### Câu hỏi 5: Anti-pattern nào cần tránh khi dùng Virtual Threads? Virtual Threads không phù hợp với khối lượng CPU-bound hay các tình huống gây "pinning". Pinning xảy ra khi Virtual Thread vẫn dính vào carrier dù bị blocking, làm mất đi lợi ích của ảo hóa. ```java // AntiPatterns.java // Examples of cases to avoid @Service public class AntiPatterns { // ANTI-PATTERN 1: CPU-intensive computation // Virtual Threads provide no benefit here public BigInteger computeFactorial(int n) { // 100% CPU, no I/O, no unmounting possible BigInteger result = BigInteger.ONE; for (int i = 2; i <= n; i++) { result = result.multiply(BigInteger.valueOf(i)); } return result; // Carrier is monopolized throughout } // ANTI-PATTERN 2: Synchronized causes pinning private final Object lock = new Object(); public void pinnedOperation() { synchronized (lock) { // PINNING: VT stays on carrier performDatabaseQuery(); // Unmounting doesn't happen! } } // SOLUTION: Use ReentrantLock private final ReentrantLock reentrantLock = new ReentrantLock(); public void unpinnedOperation() { reentrantLock.lock(); try { performDatabaseQuery(); // Unmounting possible } finally { reentrantLock.unlock(); } } } ``` Xét về mặt tài nguyên, pinning biến Virtual Thread thành Platform Thread. Nguyên nhân chính là các khối `synchronized` và lời gọi native JNI. Chuyển sang `ReentrantLock` xử lý được trường hợp đầu tiên. ### Câu hỏi 6: Làm sao phát hiện pinning trong ứng dụng? JVM cung cấp các tùy chọn chẩn đoán để xác định các trường hợp pinning. Thông tin này rất quan trọng khi di chuyển sang Virtual Threads, vì pinning có thể làm xấu đi hiệu năng thay vì cải thiện. ```java // PinningDiagnostics.java // Configuration and pinning detection public class PinningDiagnostics { // JVM option to log pinning // -Djdk.tracePinnedThreads=full (detailed) // -Djdk.tracePinnedThreads=short (summary) // Example code causing pinning public void demonstratePinning() { Thread.ofVirtual().start(() -> { synchronized (this) { // This log will appear with tracePinnedThreads enabled try { Thread.sleep(100); // Pinned during sleep! } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); } } ``` ```bash # Typical output with -Djdk.tracePinnedThreads=full Thread[#23,VirtualThread[#1]/runnable@ForkJoinPool-1-worker-1,5,CarrierThreads] java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183) java.base/java.lang.VirtualThread.parkOnCarrierThread(VirtualThread.java:661) com.example.PinningDiagnostics.demonstratePinning(PinningDiagnostics.java:15) ``` Phân tích log pinning sẽ chỉ ra các điểm nóng cần khắc phục. Một ứng dụng bị pinning thường xuyên sẽ không khai thác đầy đủ Virtual Threads, thậm chí còn chậm hơn so với Platform Threads. ## Benchmark Hiệu Năng ### Câu hỏi 7: Có thể kỳ vọng cải thiện hiệu năng tới đâu với Virtual Threads? Các benchmark cho thấy mức cải thiện đáng kể với những ứng dụng I/O-bound điển hình. Mức cải thiện phụ thuộc vào tỉ lệ thời gian I/O so với CPU và mức concurrency cần đạt. ```java // BenchmarkController.java // Endpoint for measuring performance @RestController @RequestMapping("/api/benchmark") public class BenchmarkController { private final ExternalApiClient apiClient; // Simulation of a typical endpoint with external calls @GetMapping("/user/{id}") public ResponseEntity getUser(@PathVariable Long id) { // Three sequential I/O calls UserInfo info = apiClient.fetchUserInfo(id); // ~50ms List orders = apiClient.fetchOrders(id); // ~80ms CreditScore score = apiClient.fetchCreditScore(id); // ~100ms return ResponseEntity.ok(new UserData(info, orders, score)); } } ``` ```text # Benchmark results - 10,000 concurrent requests # Configuration: 8 cores, 16GB RAM, simulated latency 230ms/request Platform Threads (pool 200): - Throughput: 850 req/s - P99 latency: 1250ms - Heap memory: 2.1GB Virtual Threads: - Throughput: 4200 req/s - P99 latency: 280ms - Heap memory: 850MB Gain: 5x throughput, 4.5x P99 latency reduction ``` Virtual Threads còn giảm tiêu thụ bộ nhớ vì mỗi thread chỉ chiếm vài KB thay vì khoảng 1 MB như Platform Thread. ### Câu hỏi 8: Cấu hình connection pool ra sao khi dùng Virtual Threads? Connection pool (HikariCP, Lettuce) trở thành nút thắt mới khi dùng Virtual Threads. Một pool 10 connection chỉ cho phép 10 truy vấn DB đồng thời, kể cả khi có hàng triệu Virtual Thread. ```yaml # application.yml # HikariCP configuration optimized for Virtual Threads spring: datasource: hikari: # More connections since Virtual Threads allow more concurrency maximum-pool-size: 50 minimum-idle: 10 # Shorter timeout due to more concurrent requests connection-timeout: 5000 # Fast validation validation-timeout: 3000 # Redis with Lettuce - already async-friendly data: redis: lettuce: pool: max-active: 50 max-idle: 20 ``` ```java // ConnectionPoolMonitor.java // Pool monitoring to avoid contention @Component public class ConnectionPoolMonitor { private final HikariDataSource dataSource; @Scheduled(fixedRate = 10000) public void logPoolStats() { HikariPoolMXBean pool = dataSource.getHikariPoolMXBean(); log.info("Pool stats - Active: {}, Idle: {}, Waiting: {}", pool.getActiveConnections(), pool.getIdleConnections(), pool.getThreadsAwaitingConnection()); // Alert if too many threads waiting if (pool.getThreadsAwaitingConnection() > 100) { log.warn("Connection pool contention detected!"); } } } ``` Kích thước pool phụ thuộc vào sức chứa của database, không phụ thuộc số lượng Virtual Thread. PostgreSQL tiêu chuẩn xử lý được khoảng 100-200 connection đang hoạt động. ### Câu hỏi 9: Đo lường tác động của Virtual Threads bằng Micrometer như thế nào? Micrometer và Spring Boot Actuator cung cấp các metric thiết yếu để đánh giá hiệu quả của Virtual Threads. Những metric này xác nhận lợi ích và bộc lộ vấn đề tiềm ẩn. ```java // VirtualThreadMetrics.java // Custom metrics for Virtual Threads @Component public class VirtualThreadMetrics { private final MeterRegistry registry; private final AtomicLong activeVirtualThreads = new AtomicLong(0); @PostConstruct public void registerMetrics() { // Active Virtual Threads counter Gauge.builder("virtual.threads.active", activeVirtualThreads, AtomicLong::get) .description("Number of active virtual threads") .register(registry); // JVM metrics for carriers Gauge.builder("virtual.threads.carriers", this::getCarrierCount) .description("Number of carrier threads") .register(registry); } private double getCarrierCount() { // Gets the ForkJoinPool carrier count return ForkJoinPool.commonPool().getPoolSize(); } // Interceptor to trace requests public void trackVirtualThread(Runnable task) { activeVirtualThreads.incrementAndGet(); try { task.run(); } finally { activeVirtualThreads.decrementAndGet(); } } } ``` ```yaml # application.yml # Metrics exposure management: endpoints: web: exposure: include: health,metrics,prometheus metrics: tags: application: ${spring.application.name} ``` Việc phân tích metric cho thấy tỉ lệ Virtual Threads / Carriers và xác định các giai đoạn xảy ra contention trên connection pool. ## Di Chuyển và Tương Thích ### Câu hỏi 10: Thư viện nào tương thích với Virtual Threads? Mức tương thích phụ thuộc vào việc dùng các khối `synchronized` và các lời gọi native. Hệ sinh thái Spring chủ yếu đã tương thích, nhưng một số thư viện đòi hỏi phiên bản cụ thể. ```java // CompatibilityCheck.java // Checking dependency compatibility @Configuration public class CompatibilityCheck { // Compatible libraries (recommended versions) // - Spring Boot 3.2+ (native support) // - HikariCP 5.1+ (ReentrantLock instead of synchronized) // - Lettuce 6.3+ (non-blocking I/O) // - Jackson 2.16+ (no synchronized) // Libraries requiring attention // - JDBC drivers: check version // - Some legacy HTTP clients @Bean public CommandLineRunner checkCompatibility() { return args -> { // Log versions for audit log.info("Java version: {}", System.getProperty("java.version")); log.info("Virtual threads available: {}", Thread.ofVirtual() != null); // Support verification if (Runtime.version().feature() < 21) { throw new IllegalStateException( "Java 21+ required for Virtual Threads"); } }; } } ``` Phần lớn các framework hiện đại đã chuyển sang `ReentrantLock`. Với các phụ thuộc cũ, một bài kiểm thử tải bằng `-Djdk.tracePinnedThreads=short` sẽ chỉ ra các vấn đề pinning. ### Câu hỏi 11: Làm thế nào để di chuyển dần dần sang Virtual Threads? Một cuộc di chuyển từng bước cho phép xác nhận lợi ích và phát hiện vấn đề mà không gây rủi ro cho production. Chiến lược được khuyến nghị là tách các endpoint dùng Virtual Threads khỏi endpoint truyền thống. ```java // DualExecutorConfig.java // Configuration for progressive migration @Configuration public class DualExecutorConfig { // Traditional executor for legacy endpoints @Bean("platformExecutor") public ExecutorService platformExecutor() { return Executors.newFixedThreadPool(200); } // Virtual Threads executor for new endpoints @Bean("virtualExecutor") public ExecutorService virtualExecutor() { return Executors.newVirtualThreadPerTaskExecutor(); } } @RestController @RequestMapping("/api/v2") public class MigratedController { @Qualifier("virtualExecutor") private final ExecutorService executor; // Endpoint migrated to Virtual Threads @GetMapping("/users/{id}") public CompletableFuture getUser(@PathVariable Long id) { return CompletableFuture.supplyAsync(() -> { // Business code unchanged return userService.findById(id); }, executor); } } ``` ```yaml # application.yml # Feature flag for migration features: virtual-threads: enabled: true endpoints: - /api/v2/** - /api/reports/** # Disable for quick rollback # features.virtual-threads.enabled: false ``` > **Lưu Ý** > > Không nên bật Virtual Threads toàn cục trước khi kiểm thử mọi phụ thuộc. Pinning có thể làm hiệu năng giảm đáng kể. ### Câu hỏi 12: Cần kiểm thử gì trước khi triển khai production? Việc kiểm chứng Virtual Threads đòi hỏi kiểm thử tải, kiểm thử pinning và kiểm thử tương thích. Các bài kiểm thử này phải mô phỏng điều kiện production thực tế. ```java // VirtualThreadLoadTest.java // Load test with JUnit and Virtual Threads @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class VirtualThreadLoadTest { @LocalServerPort private int port; @Test void shouldHandleHighConcurrency() throws Exception { int concurrentRequests = 5000; CountDownLatch latch = new CountDownLatch(concurrentRequests); AtomicInteger successCount = new AtomicInteger(0); AtomicInteger errorCount = new AtomicInteger(0); HttpClient client = HttpClient.newHttpClient(); // Launch 5000 simultaneous requests try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < concurrentRequests; i++) { executor.submit(() -> { try { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + port + "/api/test")) .build(); HttpResponse response = client.send( request, BodyHandlers.ofString()); if (response.statusCode() == 200) { successCount.incrementAndGet(); } else { errorCount.incrementAndGet(); } } catch (Exception e) { errorCount.incrementAndGet(); } finally { latch.countDown(); } }); } } latch.await(60, TimeUnit.SECONDS); // Performance assertions assertThat(successCount.get()).isGreaterThan(4900); // >98% success assertThat(errorCount.get()).isLessThan(100); } } ``` Kiểm thử cần bao quát các kịch bản contention (connection pool quá tải), timeout và tải kéo dài trong nhiều phút. ## Câu Hỏi Nâng Cao ### Câu hỏi 13: Virtual Threads tương tác với Structured Concurrency thế nào? Structured Concurrency (JEP 453) bổ sung cho Virtual Threads bằng cách bảo đảm các task đồng thời chia sẻ cùng vòng đời. Cách tiếp cận này đơn giản hóa việc xử lý lỗi và hủy bỏ. ```java // StructuredConcurrencyExample.java // Combining Virtual Threads + Structured Concurrency public class StructuredConcurrencyExample { public UserDashboard fetchDashboard(Long userId) throws Exception { // StructuredTaskScope ensures all tasks complete together try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // Three parallel tasks on Virtual Threads Subtask profileTask = scope.fork(() -> userService.getProfile(userId)); Subtask> notifTask = scope.fork(() -> notificationService.getRecent(userId)); Subtask balanceTask = scope.fork(() -> accountService.getBalance(userId)); // Wait for all tasks or fail on first error scope.join(); scope.throwIfFailed(); // All tasks succeeded - safe aggregation return new UserDashboard( profileTask.get(), notifTask.get(), balanceTask.get() ); } // If one task fails, others are automatically cancelled } } ``` Structured Concurrency ngăn ngừa rò rỉ thread và làm việc debug dễ hơn vì call stack phản ánh đúng cấu trúc logic của code. ### Câu hỏi 14: Khác biệt giữa Virtual Threads và lập trình reactive là gì? Cả hai cách tiếp cận đều giải quyết cùng một vấn đề (hiệu quả I/O) nhưng theo các mô hình lập trình khác nhau. Virtual Threads cho phép viết code imperative cổ điển, trong khi reactive đòi hỏi viết lại theo dạng stream. ```java // ComparisonService.java // Same logic: imperative vs reactive @Service public class ComparisonService { // VIRTUAL THREADS APPROACH: classic imperative code // Easy to read, debug, and maintain public UserData getUserDataImperative(Long id) { User user = userRepository.findById(id).orElseThrow(); List orders = orderRepository.findByUserId(id); PaymentInfo payment = paymentService.getInfo(id); return new UserData(user, orders, payment); } // REACTIVE APPROACH: streams and operators // More complex but native backpressure public Mono getUserDataReactive(Long id) { return userRepository.findById(id) .zipWith(orderRepository.findByUserId(id).collectList()) .zipWith(paymentService.getInfo(id)) .map(tuple -> new UserData( tuple.getT1().getT1(), tuple.getT1().getT2(), tuple.getT2() )); } } ``` | Tiêu chí | Virtual Threads | Reactive | |----------|-----------------|----------| | Đường cong học | Thấp | Cao | | Debug | Stack trace cổ điển | Phức tạp | | Backpressure | Thủ công | Native | | Hệ sinh thái | Đang phát triển | Trưởng thành | | Di chuyển legacy | Dễ | Viết lại | Virtual Threads được khuyên dùng cho ứng dụng mới và việc di chuyển. Reactive vẫn phù hợp khi cần backpressure tinh vi. ### Câu hỏi 15: Quản lý ThreadLocal cùng Virtual Threads ra sao? ThreadLocal vẫn dùng được với Virtual Threads nhưng tiêu tốn bộ nhớ cho mỗi instance. Scoped Values (JEP 446) là lựa chọn hiệu quả hơn để chia sẻ context. ```java // ThreadLocalVsScopedValue.java // Comparing context approaches public class ThreadLocalVsScopedValue { // CLASSIC APPROACH: ThreadLocal // Works but expensive with millions of VT private static final ThreadLocal requestContext = new ThreadLocal<>(); public void processWithThreadLocal(Request request) { requestContext.set(new RequestContext(request.getTraceId())); try { // Context accessible everywhere in the thread processRequest(); } finally { requestContext.remove(); // Important to prevent leaks } } // MODERN APPROACH: ScopedValue (Java 21+) // More efficient, immutable, explicit scope private static final ScopedValue CONTEXT = ScopedValue.newInstance(); public void processWithScopedValue(Request request) { ScopedValue.where(CONTEXT, new RequestContext(request.getTraceId())) .run(() -> { // Context accessible within this scope processRequest(); // No cleanup needed - automatic at scope end }); } private void processRequest() { // Context access String traceId = CONTEXT.isBound() ? CONTEXT.get().getTraceId() : "unknown"; log.info("Processing with trace: {}", traceId); } } ``` ScopedValues được khuyên dùng cho các phát triển mới. Với code legacy đang dùng ThreadLocal, nên di chuyển dần dần. ## Kết Luận Virtual Threads thay đổi cách phát triển backend Java khi cho phép viết code đơn giản mà vẫn hiệu năng cao. Điểm mấu chốt: **Nền tảng:** - Bật bằng `spring.threads.virtual.enabled=true` - Lý tưởng cho khối lượng I/O-bound (REST, DB, file) - Tránh dùng cho các phép toán nặng CPU **Hiệu năng:** - Thiết lập kích thước connection pool đúng (nút thắt mới) - Theo dõi pinning bằng `-Djdk.tracePinnedThreads` - Chuyển `synchronized` sang `ReentrantLock` **Di chuyển:** - Kiểm thử dần theo nhóm endpoint - Xác nhận tính tương thích của các phụ thuộc - Tận dụng Structured Concurrency cho thao tác song song Làm chủ Virtual Threads tạo nên sự khác biệt cho ứng viên hiểu rõ thách thức hiệu năng của ứng dụng hiện đại. Các khái niệm này nay đã trở thành điều bắt buộc trong phỏng vấn kỹ thuật Spring Boot. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/spring-boot/spring-boot-virtual-threads-interview