# Spring Boot 3.4 Virtual Threads: Interview-Fragen und Performance-Benchmarks > Java 21 Virtual Threads mit Spring Boot 3.4 meistern: 15 Interview-Fragen, Performance-Benchmarks und Migrationsmuster für erfolgreiche technische Interviews. - 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 gehören zu den bedeutendsten Neuerungen von Java 21 und werden in Spring Boot 3.4 nativ unterstützt. Dieses Feature aus dem Project Loom verändert den Umgang mit Nebenläufigkeit in Backend-Anwendungen grundlegend. Technische Interviews prüfen heute das Verständnis der internen Mechanismen, die passenden Einsatzszenarien und die typischen Fallstricke. > **Tipp zur Vorbereitung** > > Interviewer unterscheiden Kandidaten, die Virtual Threads wirklich verstehen, von denen, die sie blind verwenden. Zu wissen, wann sie NICHT eingesetzt werden sollten, ist genauso wichtig wie ihre Vorteile zu kennen. ## Grundlagen der Virtual Threads ### Frage 1: Was ist ein Virtual Thread und worin unterscheidet er sich von einem Platform Thread? Ein Virtual Thread ist ein leichtgewichtiger Thread, der von der JVM und nicht vom Betriebssystem verwaltet wird. Im Gegensatz zu Platform Threads (klassischen Threads) werden Virtual Threads nicht direkt auf OS-Threads abgebildet. Die JVM kann Millionen davon mit minimalem Speicherbedarf erzeugen. ```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(); } } } ``` Der entscheidende Unterschied liegt im Verhalten beim Blockieren. Wenn ein Platform Thread blockiert (I/O, Sleep), bleibt er an den OS-Thread gebunden. Ein Virtual Thread "unmountet" sich vom Carrier-Thread und gibt diesen für andere Virtual Threads frei. ### Frage 2: Wie aktiviert man Virtual Threads in Spring Boot 3.4? Spring Boot 3.4 reduziert die Aktivierung von Virtual Threads auf eine einzige Konfigurationseigenschaft. Das gesamte Framework passt sich automatisch an: Tomcat, REST-Controller und blockierende Aufrufe profitieren sofort von dieser Optimierung. ```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()); }; } } ``` Die Aktivierung verändert das Verhalten von Tomcat: Statt eines festen Thread-Pools erhält jede Anfrage einen eigenen Virtual Thread. Dadurch entfällt der klassische Engpass des Thread-Pools. ### Frage 3: Erkläre die Konzepte "Mounting" und "Unmounting" Mounting bezeichnet das Anheften eines Virtual Thread an einen Carrier-Thread (Platform Thread). Unmounting erfolgt während blockierender Operationen und gibt den Carrier für andere Virtual Threads frei. Dieser Mechanismus ermöglicht eine optimale Nutzung der CPU-Ressourcen. ```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(); } } ``` Dieser Mechanismus ist für Entwickler transparent. Der Code wird im klassischen imperativen Stil geschrieben, doch die JVM optimiert die Carrier-Nutzung automatisch. Ein Pool aus wenigen Carriern kann Millionen Virtual Threads bedienen. > **Technischer Hinweis** > > Die Anzahl der Carrier-Threads entspricht standardmäßig der Anzahl der CPU-Kerne. Die JVM passt diesen Pool dynamisch über den ForkJoinPool an. ## Anwendungsfälle und Anti-Patterns ### Frage 4: Wann liefern Virtual Threads Performance-Gewinne? Virtual Threads glänzen bei I/O-lastigen Workloads: externe REST-Aufrufe, Datenbankabfragen, Dateizugriffe. Solche Operationen verbringen die meiste Zeit im Wartezustand, in dem der Virtual Thread seinen Carrier freigibt. ```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); } } ``` Der Gewinn ergibt sich aus der höheren Anzahl gleichzeitiger Anfragen. Mit 200 Platform Threads und 100ms-Anfragen liegt der maximale Durchsatz bei 2.000 req/s. Mit Virtual Threads sind auf derselben Maschine 50.000+ req/s erreichbar. ### Frage 5: Welche Anti-Patterns sind bei Virtual Threads zu vermeiden? Für CPU-lastige Workloads oder Konstellationen mit "Pinning" sind Virtual Threads ungeeignet. Pinning tritt auf, wenn ein Virtual Thread trotz Blockierung an seinen Carrier gebunden bleibt und damit die Vorteile der Virtualisierung zunichtemacht. ```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(); } } } ``` Pinning macht aus einem Virtual Thread aus Ressourcensicht einen Platform Thread. Hauptursachen sind `synchronized`-Blöcke und native JNI-Aufrufe. Eine Migration zu `ReentrantLock` löst den ersten Fall. ### Frage 6: Wie erkennt man Pinning in einer Anwendung? Die JVM bietet Diagnose-Optionen, um Pinning-Fälle zu identifizieren. Diese Informationen sind bei der Migration zu Virtual Threads unerlässlich, da Pinning die Performance verschlechtern statt verbessern kann. ```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) ``` Die Analyse der Pinning-Logs offenbart die Hotspots, die korrigiert werden müssen. Eine Anwendung mit häufigem Pinning nutzt Virtual Threads nicht aus und kann sogar langsamer laufen als mit Platform Threads. ## Performance-Benchmarks ### Frage 7: Welche Performance-Gewinne sind mit Virtual Threads zu erwarten? Benchmarks zeigen erhebliche Verbesserungen bei typischen I/O-lastigen Anwendungen. Die Gewinne hängen vom Verhältnis I/O-Zeit zu CPU-Zeit und vom benötigten Concurrency-Level ab. ```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 senken außerdem den Speicherbedarf, da jeder Thread nur wenige KB statt etwa 1 MB wie ein Platform Thread belegt. ### Frage 8: Wie konfiguriert man Connection Pools mit Virtual Threads? Connection Pools (HikariCP, Lettuce) werden mit Virtual Threads zum neuen Engpass. Ein Pool mit 10 Verbindungen begrenzt die parallelen Datenbankabfragen auf 10, selbst bei Millionen Virtual Threads. ```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!"); } } } ``` Die Pool-Größe richtet sich nach der Datenbankkapazität, nicht nach der Anzahl der Virtual Threads. Eine Standard-PostgreSQL-Instanz verkraftet rund 100-200 aktive Verbindungen. ### Frage 9: Wie misst man die Wirkung von Virtual Threads mit Micrometer? Micrometer und Spring Boot Actuator liefern essenzielle Metriken zur Bewertung der Effektivität von Virtual Threads. Diese Metriken bestätigen die Gewinne und decken potenzielle Probleme auf. ```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} ``` Die Auswertung der Metriken zeigt das Verhältnis Virtual Threads / Carriers und identifiziert Phasen mit Contention auf dem Connection Pool. ## Migration und Kompatibilität ### Frage 10: Welche Bibliotheken sind mit Virtual Threads kompatibel? Die Kompatibilität hängt vom Einsatz von `synchronized`-Blöcken und nativen Aufrufen ab. Das Spring-Ökosystem ist weitgehend kompatibel, einige Bibliotheken erfordern jedoch bestimmte Versionen. ```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"); } }; } } ``` Die meisten modernen Frameworks sind bereits zu `ReentrantLock` migriert. Für ältere Abhängigkeiten zeigt ein Lasttest mit `-Djdk.tracePinnedThreads=short` Pinning-Probleme auf. ### Frage 11: Wie migriert man schrittweise zu Virtual Threads? Eine schrittweise Migration validiert die Gewinne und identifiziert Probleme, ohne die Produktion zu gefährden. Die empfohlene Strategie trennt Virtual-Thread-Endpunkte von klassischen. ```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 ``` > **Achtung** > > Virtual Threads sollten nicht global aktiviert werden, bevor alle Abhängigkeiten getestet wurden. Pinning kann die Performance erheblich verschlechtern. ### Frage 12: Welche Tests sind vor dem Produktionseinsatz erforderlich? Die Validierung von Virtual Threads erfordert Lasttests, Pinning-Tests und Kompatibilitätstests. Diese Tests müssen realistische Produktionsbedingungen abbilden. ```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); } } ``` Die Tests sollten Contention-Szenarien (übersättigter Connection Pool), Timeouts und Dauerlast über mehrere Minuten abdecken. ## Fortgeschrittene Fragen ### Frage 13: Wie interagieren Virtual Threads mit Structured Concurrency? Structured Concurrency (JEP 453) ergänzt Virtual Threads, indem sie sicherstellt, dass nebenläufige Aufgaben denselben Lebenszyklus teilen. Dieser Ansatz vereinfacht Fehlerbehandlung und Cancelation. ```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 verhindert Thread-Lecks und vereinfacht das Debugging, da der Call Stack die logische Code-Struktur widerspiegelt. ### Frage 14: Worin unterscheiden sich Virtual Threads und reaktive Programmierung? Beide Ansätze lösen dasselbe Problem (I/O-Effizienz), verfolgen aber unterschiedliche Programmiermodelle. Virtual Threads erlauben klassischen imperativen Code, während reaktive Programmierung eine Umstellung auf Streams erfordert. ```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() )); } } ``` | Kriterium | Virtual Threads | Reaktiv | |-----------|-----------------|---------| | Lernkurve | Niedrig | Hoch | | Debugging | Klassische Stack Traces | Komplex | | Backpressure | Manuell | Nativ | | Ökosystem | Wachsend | Reif | | Legacy-Migration | Einfach | Neuschreiben | Für neue Anwendungen und Migrationen werden Virtual Threads empfohlen. Reaktive Programmierung bleibt sinnvoll, wenn ausgefeiltes Backpressure benötigt wird. ### Frage 15: Wie geht man mit ThreadLocal bei Virtual Threads um? ThreadLocal funktioniert mit Virtual Threads, beansprucht jedoch Speicher pro Instanz. Scoped Values (JEP 446) bieten eine effizientere Alternative für die Kontextweitergabe. ```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); } } ``` Für Neuentwicklungen sind ScopedValues empfohlen. Bei Legacy-Code mit ThreadLocal empfiehlt sich eine schrittweise Migration. ## Fazit Virtual Threads verändern die Java-Backend-Entwicklung, indem sie einfachen und performanten Code ermöglichen. Wichtige Erkenntnisse: **Grundlagen:** - Aktivierung mit `spring.threads.virtual.enabled=true` - Ideal für I/O-lastige Workloads (REST, DB, Dateien) - Ungeeignet für CPU-intensive Berechnungen **Performance:** - Connection Pools korrekt dimensionieren (neuer Engpass) - Pinning mit `-Djdk.tracePinnedThreads` überwachen - `synchronized` zu `ReentrantLock` migrieren **Migration:** - Schrittweise nach Endpunkt-Gruppen testen - Kompatibilität der Abhängigkeiten validieren - Structured Concurrency für parallele Operationen nutzen Die Beherrschung von Virtual Threads hebt Kandidaten hervor, die die Performance-Herausforderungen moderner Anwendungen verstehen. Diese Konzepte sind in Spring-Boot-Interviews inzwischen unverzichtbar. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/spring-boot/spring-boot-virtual-threads-interview