Spring Boot 3.4 Virtual Threads: 面接質問とパフォーマンスベンチマーク
Spring Boot 3.4でJava 21 Virtual Threadsを使いこなしましょう。15の面接質問、パフォーマンスベンチマーク、技術面接を突破するための移行パターンを解説します。

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を生成できます。
// 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コントローラー、ブロッキング呼び出しが即座にこの最適化の恩恵を受けます。
# 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// 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リソースを最大限に活用できます。
// 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はキャリアを解放します。
// 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がキャリアに張り付いてしまい、仮想化の利点が打ち消される現象です。
// 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は性能を改善するどころか低下させてしまう可能性があるためです。
// 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();
}
}
});
}
}# 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より遅くなることさえあります。
Spring Bootの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
パフォーマンスベンチマーク
質問7: Virtual Threadsで期待できる性能向上はどの程度か
ベンチマークでは、典型的なI/Oバウンドアプリケーションで顕著な改善が示されます。改善幅はI/O時間とCPU時間の比率、必要となる並行度の水準に依存します。
// 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));
}
}# 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 reductionVirtual Threadsはメモリ消費も抑えます。スレッドあたり数KBしか確保せず、Platform Threadのおよそ1MBに比べてはるかに軽量です。
質問8: Virtual Threadsを使う場合のコネクションプールの設定方法
Virtual Threadsを利用すると、HikariCPやLettuceなどのコネクションプールが新たなボトルネックとなります。10コネクションのプールでは、たとえVirtual Threadが数百万あってもDBクエリの同時実行は10件に制限されます。
# 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// 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の効果を評価するための重要なメトリクスを提供します。これらのメトリクスにより、利点を裏付け、潜在的な問題を見つけられます。
// 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();
}
}
}# 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エコシステムは概ね互換性を備えていますが、一部のライブラリは特定バージョンを必要とします。
// 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を使うエンドポイントを従来型のものから分離することです。
// 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);
}
}# 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テスト、互換性テストが欠かせません。これらのテストは現実の本番条件を模擬する必要があります。
// 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);
}
}テストでは競合シナリオ(コネクションプール枯渇)、タイムアウト、数分間継続する負荷を網羅する必要があります。
高度な質問
質問13: Virtual ThreadsとStructured Concurrencyの関係は
Structured Concurrency(JEP 453)はVirtual Threadsを補完し、並行タスクが同じライフサイクルを共有することを保証します。このアプローチはエラー処理とキャンセル処理を簡潔にします。
// 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はスレッドリークを防ぎ、コールスタックがコードの論理構造を反映するためデバッグが容易になります。
質問14: Virtual Threadsとリアクティブプログラミングの違いは
どちらも同じ問題(I/O効率)に取り組みますが、プログラミングモデルが異なります。Virtual Threadsは従来の命令型コードを許容しますが、リアクティブはストリーム形式での書き直しを必要とします。
// 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()
));
}
}| 観点 | Virtual Threads | リアクティブ | |------|-----------------|------------| | 学習コスト | 低い | 高い | | デバッグ | 古典的なスタックトレース | 複雑 | | バックプレッシャー | 手動 | ネイティブ | | エコシステム | 拡大中 | 成熟 | | レガシー移行 | 容易 | 書き直し |
新規アプリケーションや移行ではVirtual Threadsが推奨されます。リアクティブは精緻なバックプレッシャーが必要なケースで依然有用です。
質問15: Virtual ThreadsでのThreadLocalの扱いは
ThreadLocalはVirtual Threadsでも使えますが、インスタンスごとにメモリを消費します。Scoped Values(JEP 446)はコンテキスト共有のためのより効率的な代替手段です。
// 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は新規開発で推奨されます。ThreadLocalを利用するレガシーコードは段階的な移行が望ましいです。
Spring Bootの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
まとめ
Virtual ThreadsはシンプルかつハイパフォーマンスなコードでJavaバックエンド開発を変革します。重要なポイント:
基礎:
spring.threads.virtual.enabled=trueで有効化- I/Oバウンドな処理(REST、DB、ファイル)に最適
- CPU負荷の高い計算には不向き
性能:
- コネクションプールを適切にサイジング(新たなボトルネック)
-Djdk.tracePinnedThreadsでpinningを監視synchronizedをReentrantLockへ移行
移行:
- エンドポイント単位で段階的にテスト
- 依存関係の互換性を検証
- 並列処理にはStructured Concurrencyを活用
Virtual Threadsの理解は、現代アプリケーションの性能課題を捉えている候補者を際立たせます。これらの概念はSpring Bootの技術面接で必須の知識となっています。
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
タグ
共有
関連記事

Spring Modulith: モジュラーモノリスアーキテクチャ解説
Spring Modulith で Java のモジュラーモノリスを構築する方法を学びます。アーキテクチャ、モジュール、非同期イベント、Spring Boot 3 のコード例によるテスト。

Spring Batch 5 面接対策: パーティショニング・チャンク・フォールトトレランス
Spring Batch 5 の面接を制覇する15の必須質問。パーティショニング、チャンク処理、フォールトトレランスを Java 21 のコード例と共に解説します。

Testcontainers Spring Boot:苦痛のない統合テスト
Spring Boot 3.4でTestcontainersを設定する完全ガイド。信頼性が高く再現可能な統合テストのために、PostgreSQL、Redis、KafkaをDockerコンテナで実行します。