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.

Spring Boot 3.4 Virtual Threads: Interview-Fragen und Performance-Benchmarks

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.

VirtualThreadDemo.javajava
// 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
WebConfig.javajava
// 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.

MountingDemo.javajava
// 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.

IOBoundService.javajava
// 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.

AntiPatterns.javajava
// 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.

PinningDiagnostics.javajava
// 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.

Bereit für deine Spring Boot-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

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.

BenchmarkController.javajava
// 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<UserData> getUser(@PathVariable Long id) {
        // Three sequential I/O calls
        UserInfo info = apiClient.fetchUserInfo(id);        // ~50ms
        List<Order> 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
ConnectionPoolMonitor.javajava
// 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.

VirtualThreadMetrics.javajava
// 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.

CompatibilityCheck.javajava
// 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.

DualExecutorConfig.javajava
// 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<User> 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.

VirtualThreadLoadTest.javajava
// 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<String> 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.

StructuredConcurrencyExample.javajava
// 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<UserProfile> profileTask = scope.fork(() ->
                userService.getProfile(userId));

            Subtask<List<Notification>> notifTask = scope.fork(() ->
                notificationService.getRecent(userId));

            Subtask<AccountBalance> 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.

ComparisonService.javajava
// 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<Order> 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<UserData> 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.

ThreadLocalVsScopedValue.javajava
// Comparing context approaches
public class ThreadLocalVsScopedValue {

    // CLASSIC APPROACH: ThreadLocal
    // Works but expensive with millions of VT
    private static final ThreadLocal<RequestContext> 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<RequestContext> 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.

Bereit für deine Spring Boot-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

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.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#spring boot
#virtual threads
#java 21
#project loom
#performance

Teilen

Verwandte Artikel