# Spring Boot 3.4 Virtual Threads: 面接質問とパフォーマンスベンチマーク > Spring Boot 3.4でJava 21 Virtual Threadsを使いこなしましょう。15の面接質問、パフォーマンスベンチマーク、技術面接を突破するための移行パターンを解説します。 - 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はJava 21における最も重要な進歩のひとつであり、Spring Boot 3.4はそれをネイティブに統合しています。Project Loomのこの機能は、バックエンドアプリケーションにおける並行処理のあり方を大きく変えます。技術面接では、内部メカニズムの理解、適切なユースケース、避けるべき典型的な落とし穴が問われるようになっています。 > **準備のヒント** > > 面接官はVirtual Threadsを本当に理解している候補者と、何となく使っているだけの候補者を見分けます。利点を知ることと同じくらい、いつ使うべきでないかを把握することが重要です。 ## Virtual Threadsの基礎 ### 質問1: Virtual ThreadとPlatform Threadはどう違うのか Virtual ThreadはOSではなくJVMが管理する軽量なスレッドです。Platform Threads(従来のスレッド)とは異なり、Virtual ThreadはOSスレッドへ直接マッピングされません。JVMはわずかなメモリ消費で数百万ものVirtual Threadを生成できます。 ```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(); } } } ``` 本質的な違いはブロッキング時のふるまいにあります。Platform ThreadはI/OやsleepでブロックされてもなおキャリアであるOSスレッドを保持し続けます。一方Virtual Threadはキャリアスレッドからアンマウントし、他のVirtual Threadがそのキャリアを利用できるようにします。 ### 質問2: Spring Boot 3.4でVirtual Threadsを有効化する方法 Spring Boot 3.4ではVirtual Threadsの有効化が単一の設定プロパティに集約されています。フレームワーク全体が自動的に追従し、Tomcat、RESTコントローラー、ブロッキング呼び出しが即座にこの最適化の恩恵を受けます。 ```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()); }; } } ``` 有効化によりTomcatの動作が変わります。固定サイズのスレッドプールではなく、リクエストごとに専用のVirtual Threadが割り当てられるためです。これにより従来のスレッドプールのボトルネックが解消されます。 ### 質問3: 「mounting」と「unmounting」の概念を説明してください MountingはVirtual Threadをキャリアスレッド(Platform Thread)に取り付けることを指します。UnmountingはブロッキングオペレーションでキャリアをほかのVirtual Threadに解放する動きです。この仕組みにより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(); } } ``` この仕組みは開発者には透過的です。コードは従来どおりの命令型スタイルで記述しますが、JVMがキャリア利用を自動最適化します。少数のキャリアからなるプールでも数百万のVirtual Threadを処理できます。 > **技術メモ** > > キャリアスレッドの数は標準でCPUコア数と一致します。JVMはForkJoinPoolを通じてこのプールを動的に調整します。 ## ユースケースとアンチパターン ### 質問4: Virtual Threadsはどのようなときに性能向上をもたらすのか Virtual Threadsは外部REST呼び出し、データベースクエリ、ファイル読み込みなどI/Oバウンドの処理で真価を発揮します。これらの処理は大半の時間を待機に費やし、その間Virtual Threadはキャリアを解放します。 ```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); } } ``` 性能向上は、より多くの同時リクエストを処理できることに由来します。Platform Thread 200本と100msのリクエストでは最大スループットが2,000 req/sですが、Virtual Threadsでは同一マシン上で50,000 req/s超に達する可能性があります。 ### 質問5: Virtual Threadsで避けるべきアンチパターンは何か Virtual ThreadsはCPUバウンドのワークロードや「pinning」が発生するケースには不向きです。pinningは、ブロッキング中にもVirtual Threadがキャリアに張り付いてしまい、仮想化の利点が打ち消される現象です。 ```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はVirtual ThreadをPlatform Threadに変えてしまいます。主な原因は`synchronized`ブロックとJNIによるネイティブ呼び出しです。`ReentrantLock`への移行で前者は解消できます。 ### 質問6: アプリケーションでpinningを検出する方法 JVMはpinningのケースを特定する診断オプションを備えています。Virtual Threadsへ移行する際、この情報は欠かせません。pinningは性能を改善するどころか低下させてしまう可能性があるためです。 ```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) ``` pinningログを分析することで、修正すべきホットスポットが浮かび上がります。pinningが頻発するアプリケーションはVirtual Threadsの利点を活かせず、Platform Threadsより遅くなることさえあります。 ## パフォーマンスベンチマーク ### 質問7: Virtual Threadsで期待できる性能向上はどの程度か ベンチマークでは、典型的なI/Oバウンドアプリケーションで顕著な改善が示されます。改善幅はI/O時間とCPU時間の比率、必要となる並行度の水準に依存します。 ```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はメモリ消費も抑えます。スレッドあたり数KBしか確保せず、Platform Threadのおよそ1MBに比べてはるかに軽量です。 ### 質問8: Virtual Threadsを使う場合のコネクションプールの設定方法 Virtual Threadsを利用すると、HikariCPやLettuceなどのコネクションプールが新たなボトルネックとなります。10コネクションのプールでは、たとえVirtual Threadが数百万あってもDBクエリの同時実行は10件に制限されます。 ```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!"); } } } ``` プールサイズの目安はデータベースのキャパシティに依存し、Virtual Thread数とは関係ありません。標準的なPostgreSQLは100〜200のアクティブコネクションを扱える程度です。 ### 質問9: MicrometerでVirtual Threadsの効果を測定するには MicrometerとSpring Boot ActuatorはVirtual Threadsの効果を評価するための重要なメトリクスを提供します。これらのメトリクスにより、利点を裏付け、潜在的な問題を見つけられます。 ```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} ``` メトリクスを分析するとVirtual Threads/Carriersの比率が見え、コネクションプールでの競合期間も特定できます。 ## 移行と互換性 ### 質問10: Virtual Threadsと互換性のあるライブラリは何か 互換性は`synchronized`ブロックの利用とネイティブ呼び出しの有無に左右されます。Springエコシステムは概ね互換性を備えていますが、一部のライブラリは特定バージョンを必要とします。 ```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"); } }; } } ``` 大半のモダンなフレームワークはすでに`ReentrantLock`へ移行済みです。レガシーな依存関係には`-Djdk.tracePinnedThreads=short`を有効にしたロードテストでpinning問題を洗い出せます。 ### 質問11: Virtual Threadsへ段階的に移行する方法 段階的な移行は、本番に影響を与えずに利点を確認し問題を洗い出すために役立ちます。推奨される戦略は、Virtual Threadsを使うエンドポイントを従来型のものから分離することです。 ```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 ``` > **注意** > > 依存関係の検証を済ませる前にグローバルでVirtual Threadsを有効化すべきではありません。pinningによって性能が大幅に劣化する恐れがあります。 ### 質問12: 本番デプロイ前に必要なテストは何か Virtual Threadsの検証にはロードテスト、pinningテスト、互換性テストが欠かせません。これらのテストは現実の本番条件を模擬する必要があります。 ```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); } } ``` テストでは競合シナリオ(コネクションプール枯渇)、タイムアウト、数分間継続する負荷を網羅する必要があります。 ## 高度な質問 ### 質問13: Virtual ThreadsとStructured Concurrencyの関係は Structured Concurrency(JEP 453)はVirtual Threadsを補完し、並行タスクが同じライフサイクルを共有することを保証します。このアプローチはエラー処理とキャンセル処理を簡潔にします。 ```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はスレッドリークを防ぎ、コールスタックがコードの論理構造を反映するためデバッグが容易になります。 ### 質問14: Virtual Threadsとリアクティブプログラミングの違いは どちらも同じ問題(I/O効率)に取り組みますが、プログラミングモデルが異なります。Virtual Threadsは従来の命令型コードを許容しますが、リアクティブはストリーム形式での書き直しを必要とします。 ```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() )); } } ``` | 観点 | Virtual Threads | リアクティブ | |------|-----------------|------------| | 学習コスト | 低い | 高い | | デバッグ | 古典的なスタックトレース | 複雑 | | バックプレッシャー | 手動 | ネイティブ | | エコシステム | 拡大中 | 成熟 | | レガシー移行 | 容易 | 書き直し | 新規アプリケーションや移行ではVirtual Threadsが推奨されます。リアクティブは精緻なバックプレッシャーが必要なケースで依然有用です。 ### 質問15: Virtual ThreadsでのThreadLocalの扱いは ThreadLocalはVirtual Threadsでも使えますが、インスタンスごとにメモリを消費します。Scoped Values(JEP 446)はコンテキスト共有のためのより効率的な代替手段です。 ```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は新規開発で推奨されます。ThreadLocalを利用するレガシーコードは段階的な移行が望ましいです。 ## まとめ Virtual ThreadsはシンプルかつハイパフォーマンスなコードでJavaバックエンド開発を変革します。重要なポイント: **基礎:** - `spring.threads.virtual.enabled=true`で有効化 - I/Oバウンドな処理(REST、DB、ファイル)に最適 - CPU負荷の高い計算には不向き **性能:** - コネクションプールを適切にサイジング(新たなボトルネック) - `-Djdk.tracePinnedThreads`でpinningを監視 - `synchronized`を`ReentrantLock`へ移行 **移行:** - エンドポイント単位で段階的にテスト - 依存関係の互換性を検証 - 並列処理にはStructured Concurrencyを活用 Virtual Threadsの理解は、現代アプリケーションの性能課題を捉えている候補者を際立たせます。これらの概念はSpring Bootの技術面接で必須の知識となっています。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/spring-boot/spring-boot-virtual-threads-interview