Spring Boot 3.4 Virtual Threads: Pytania Rekrutacyjne i Benchmarki Wydajności

Opanuj Virtual Threads z Java 21 i Spring Boot 3.4: 15 pytań rekrutacyjnych, benchmarki wydajności oraz wzorce migracji do zaliczenia rozmów technicznych.

Spring Boot 3.4 Virtual Threads: pytania rekrutacyjne i benchmarki wydajności

Virtual Threads to jedno z najważniejszych usprawnień Java 21, a Spring Boot 3.4 integruje je natywnie. Ten element Project Loom zmienia sposób obsługi współbieżności w aplikacjach backendowych. Rozmowy techniczne sprawdzają dziś znajomość mechanizmów wewnętrznych, odpowiednich przypadków użycia oraz typowych pułapek.

Wskazówka Przygotowawcza

Rekruterzy odróżniają kandydatów, którzy rozumieją Virtual Threads, od tych, którzy używają ich na ślepo. Wiedza o tym, kiedy ich NIE stosować, jest równie istotna jak znajomość ich zalet.

Podstawy Virtual Threads

Pytanie 1: Czym jest Virtual Thread i czym różni się od Platform Thread?

Virtual Thread to lekki wątek zarządzany przez JVM, a nie przez system operacyjny. W przeciwieństwie do Platform Threads (klasycznych wątków) Virtual Threads nie są bezpośrednio mapowane na wątki systemowe. JVM może utworzyć ich miliony przy minimalnym zużyciu pamięci.

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();
        }
    }
}

Kluczowa różnica leży w zachowaniu podczas blokady. Gdy Platform Thread się blokuje (I/O, sleep), pozostaje przypięty do wątku systemowego. Virtual Thread natomiast "odpina się" od wątku-nośnika, zwalniając go dla innych Virtual Threads.

Pytanie 2: Jak włączyć Virtual Threads w Spring Boot 3.4?

Spring Boot 3.4 sprowadza włączenie Virtual Threads do jednej właściwości konfiguracyjnej. Cały framework dostosowuje się automatycznie: Tomcat, kontrolery REST i wywołania blokujące natychmiast korzystają z tej optymalizacji.

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());
        };
    }
}

Włączenie zmienia działanie Tomcata: zamiast stałej puli wątków każde żądanie otrzymuje własny Virtual Thread. To podejście eliminuje klasyczne wąskie gardło puli wątków.

Pytanie 3: Wyjaśnij pojęcia "mounting" i "unmounting"

Mounting oznacza dołączenie Virtual Thread do wątku-nośnika (Platform Thread). Unmounting następuje podczas operacji blokujących i zwalnia nośnik dla innych Virtual Threads. Mechanizm ten zapewnia optymalne wykorzystanie zasobów CPU.

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();
    }
}

Mechanizm ten jest niewidoczny dla programisty. Kod pisze się w klasycznym stylu imperatywnym, ale JVM automatycznie optymalizuje wykorzystanie nośników. Pula kilku nośników może obsłużyć miliony Virtual Threads.

Uwaga Techniczna

Liczba wątków-nośników odpowiada domyślnie liczbie rdzeni CPU. JVM dostosowuje tę pulę dynamicznie poprzez ForkJoinPool.

Przypadki Użycia i Antywzorce

Pytanie 4: Kiedy Virtual Threads dają zysk wydajności?

Virtual Threads błyszczą w obciążeniach I/O-bound: zewnętrzne wywołania REST, zapytania do bazy, odczyt plików. Operacje te większość czasu spędzają w oczekiwaniu, podczas którego Virtual Thread zwalnia swój nośnik.

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);
    }
}

Zysk wynika z obsługi znacznie większej liczby równoczesnych żądań. Przy 200 Platform Threads i żądaniach 100ms maksymalna przepustowość wynosi 2 000 req/s. Z Virtual Threads na tej samej maszynie można osiągnąć ponad 50 000 req/s.

Pytanie 5: Jakich antywzorców unikać przy Virtual Threads?

Virtual Threads nie nadają się do obciążeń CPU-bound ani do sytuacji powodujących "pinning". Pinning występuje, gdy Virtual Thread mimo blokady pozostaje przypięty do swojego nośnika, niwecząc korzyści wirtualizacji.

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 sprawia, że z punktu widzenia zasobów Virtual Thread staje się Platform Threadem. Główne przyczyny to bloki synchronized oraz natywne wywołania JNI. Migracja do ReentrantLock rozwiązuje pierwszy przypadek.

Pytanie 6: Jak wykrywać pinning w aplikacji?

JVM udostępnia opcje diagnostyczne pozwalające zidentyfikować przypadki pinningu. Te informacje są kluczowe podczas migracji do Virtual Threads, ponieważ pinning może pogorszyć wydajność zamiast ją poprawić.

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)

Analiza logów pinningu ujawnia hotspoty wymagające poprawy. Aplikacja z częstym pinningiem nie wykorzystuje w pełni Virtual Threads, a może być nawet wolniejsza niż z Platform Threads.

Gotowy na rozmowy o Spring Boot?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Benchmarki Wydajności

Pytanie 7: Jakich zysków wydajności można oczekiwać z Virtual Threads?

Benchmarki pokazują znaczące poprawy w typowych aplikacjach I/O-bound. Zyski zależą od stosunku czasu I/O do czasu CPU oraz od wymaganego poziomu współbieżności.

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 zmniejszają również zużycie pamięci, ponieważ każdy wątek alokuje tylko kilka KB zamiast około 1 MB jak Platform Thread.

Pytanie 8: Jak konfigurować pule połączeń przy Virtual Threads?

Pule połączeń (HikariCP, Lettuce) stają się nowym wąskim gardłem przy Virtual Threads. Pula 10 połączeń ogranicza do 10 jednoczesnych zapytań do bazy, nawet przy milionach 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!");
        }
    }
}

Rozmiar puli zależy od pojemności bazy danych, a nie od liczby Virtual Threads. Standardowy PostgreSQL obsługuje około 100-200 aktywnych połączeń.

Pytanie 9: Jak mierzyć wpływ Virtual Threads za pomocą Micrometera?

Micrometer i Spring Boot Actuator dostarczają kluczowych metryk do oceny skuteczności Virtual Threads. Te metryki potwierdzają zyski i ujawniają potencjalne problemy.

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}

Analiza metryk ujawnia stosunek Virtual Threads do nośników i wskazuje okresy rywalizacji o pulę połączeń.

Migracja i Kompatybilność

Pytanie 10: Które biblioteki są kompatybilne z Virtual Threads?

Kompatybilność zależy od użycia bloków synchronized oraz wywołań natywnych. Ekosystem Spring jest w dużej mierze zgodny, ale niektóre biblioteki wymagają określonych wersji.

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");
            }
        };
    }
}

Większość nowoczesnych frameworków przeszła już na ReentrantLock. Dla starszych zależności test obciążeniowy z -Djdk.tracePinnedThreads=short ujawnia problemy z pinningiem.

Pytanie 11: Jak migrować stopniowo do Virtual Threads?

Stopniowa migracja pozwala potwierdzić zyski i zidentyfikować problemy bez ryzyka dla produkcji. Zalecana strategia oddziela endpointy z Virtual Threads od tradycyjnych.

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
Uwaga

Nie należy włączać Virtual Threads globalnie przed przetestowaniem wszystkich zależności. Pinning może istotnie pogorszyć wydajność.

Pytanie 12: Jakie testy wykonać przed wdrożeniem produkcyjnym?

Walidacja Virtual Threads wymaga testów obciążeniowych, testów pinningu oraz testów kompatybilności. Te testy muszą odwzorowywać realistyczne warunki produkcyjne.

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);
    }
}

Testy powinny obejmować scenariusze rywalizacji (przeciążona pula połączeń), timeouty oraz utrzymane obciążenie przez kilka minut.

Pytania Zaawansowane

Pytanie 13: Jak Virtual Threads współpracują ze Structured Concurrency?

Structured Concurrency (JEP 453) uzupełnia Virtual Threads, gwarantując, że współbieżne zadania dzielą ten sam cykl życia. To podejście upraszcza obsługę błędów i anulowanie.

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 zapobiega wyciekom wątków i upraszcza debugowanie, ponieważ stos wywołań odzwierciedla logiczną strukturę kodu.

Pytanie 14: Czym różnią się Virtual Threads od programowania reaktywnego?

Oba podejścia rozwiązują ten sam problem (efektywność I/O), lecz różnymi modelami programowania. Virtual Threads pozwalają na klasyczny kod imperatywny, a programowanie reaktywne wymaga przepisania go w postaci strumieni.

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()
            ));
    }
}

| Kryterium | Virtual Threads | Reaktywne | |-----------|-----------------|-----------| | Krzywa uczenia | Niska | Wysoka | | Debugowanie | Klasyczne stack trace | Złożone | | Backpressure | Manualne | Natywne | | Ekosystem | Rosnący | Dojrzały | | Migracja legacy | Prosta | Przepisanie |

Dla nowych aplikacji i migracji rekomenduje się Virtual Threads. Programowanie reaktywne pozostaje sensowne tam, gdzie wymagane jest zaawansowane backpressure.

Pytanie 15: Jak obsługiwać ThreadLocal z Virtual Threads?

ThreadLocal działa z Virtual Threads, ale zużywa pamięć dla każdej instancji. Scoped Values (JEP 446) oferują efektywniejszą alternatywę dla współdzielenia kontekstu.

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);
    }
}

ScopedValues są zalecane dla nowych projektów. Dla starszego kodu z ThreadLocal warto zaplanować stopniową migrację.

Gotowy na rozmowy o Spring Boot?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Podsumowanie

Virtual Threads zmieniają backendowe programowanie w Javie, umożliwiając pisanie prostego i wydajnego kodu. Najważniejsze wnioski:

Podstawy:

  • Aktywacja przez spring.threads.virtual.enabled=true
  • Idealne dla obciążeń I/O-bound (REST, BD, pliki)
  • Unikać dla obliczeń intensywnie obciążających CPU

Wydajność:

  • Właściwie dobrać rozmiar pul połączeń (nowe wąskie gardło)
  • Monitorować pinning poprzez -Djdk.tracePinnedThreads
  • Migrować synchronized na ReentrantLock

Migracja:

  • Testować stopniowo grupami endpointów
  • Walidować zgodność zależności
  • Wykorzystać Structured Concurrency w operacjach równoległych

Znajomość Virtual Threads wyróżnia kandydatów rozumiejących wyzwania wydajnościowe nowoczesnych aplikacji. Te zagadnienia są dziś nieodzowne w technicznych rozmowach o Spring Boot.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

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

Udostępnij

Powiązane artykuły