Java の仮想スレッド:スケーラビリティと複雑性のバランス
従来の並行性の問題
Java アプリケーションは伝統的に プラットフォームスレッド(Platform Threads または従来のスレッド)に依存して並行処理を行ってきました。
プラットフォームスレッド(PT):
- OS 管理スレッドのラッパーで 1 マッピング。
- 高価で重く、各スレッドが約 1-2 MB のメモリと OS リソースを消費。
- システムリソースに制限される。多すぎる作成は
OutOfMemoryErrorまたはコンテキストスイッチの飽和を引き起こす。 - Web アプリでは "リクエストごとにスレッド"(Thread-per-Request)モデルが直感的だが、高並行(例:>10k 同時リクエスト)ではスケールしない。
必要性: マイクロサービス、ストリーミング、トレーディング API などの高並行シナリオでは、低遅延と高スケーラビリティが必要。リアクティブプログラミング(Project Reactor、RxJava)などのソリューションはスループットを向上させるが、コードとデバッグを複雑にする。
仮想スレッド(VT)とは何か?
Project Loom によって導入され、JDK 21(JEP 444)で安定化された仮想スレッドは、非同期プログラミングなしで大規模並行を実現する Java のソリューションです。
主な特徴:
- 軽量スレッド: JVM が管理する
java.lang.Threadのインスタンス(約 1 KB あたり)。 - 数百万スレッド: メモリを使い果たさずに 数百万 のスレッドを作成可能。
- I/O 結合スケーラビリティ: ブロッキングタスク(CPU 結合ではない)のスループットに最適化。
- 完全互換: 既存 Thread コードのドロップイン代替。Go の goroutines に類似。
- JDK 21+ で利用可能: JDK 21 で安定。JDK 22-25 で継続的に改善。
内部動作:M スケジューリング
M
マッピング: M 個の仮想スレッド(数百万)を N 個のプラットフォームスレッド(CPU コア)にマッピング。- キャリア: VT を「運ぶ」プラットフォームスレッド。
- マウント/アンマウント: VT が実行のためにキャリアにマウント。I/O ブロックでアンマウント(
sleep()、ソケット、DB クエリ)。 - JVM スケジューラ: 動的にキャリアを再割り当て。I/O 待機中はキャリアが解放。
- コンティニュエーション: I/O 完了で VT がスケジューラに戻り、任意の利用可能キャリアに再マウント。
これで CPU 使用率を最大化:キャリアは常にビジー。
最近の更新:
- JDK 24(JEP 491):
synchronizedブロックでの ピン留め を解消(VT が正しくアンマウント)。 - JDK 25(JEP 506):
ThreadLocalの代わりの不変代替として スコープ値、VT でのメモリリーク防止。
実践的例
I/O 結合(HTTP、DB、ファイル)向けに最適化。
例 1:シンプル仮想スレッド
Thread.startVirtualThread(() -> {
System.out.println("VT からの挨拶!" + Thread.currentThread());
});例 2:ExecutorService(高並行)
import java.util.concurrent.Executors;
import java.time.Duration;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 10k 同時タスク
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // I/O シミュレーション
// ビジネスロジック
})
);
} // 自動完了待ち例 3:Spring Boot + VT(実ベンチマーク)
最近のベンチマーク(JDK 24 + Spring Boot + Postgres)では、VT が 10k req/s でプラットフォームスレッド比約 50% の遅延削減。
@RestController
public class ApiController {
@GetMapping("/data")
String getData() {
// VT が Tomcat/Undertow 設定で自動処理
return fetchFromDb(); // ブロックだがスケーラブル
}
}高度な考慮事項とベストプラクティス
CPU 結合 vs I/O 結合
| タイプ | 推奨 | 理由 |
|---|---|---|
| I/O 結合(DB、HTTP) | VT + newVirtualThreadPerTaskExecutor() | 大規模スケーラビリティ |
| CPU 結合(暗号化、ML) | プラットフォームスレッドプール = CPU コア数 | キャリアスタベーション を回避 |
問題と解決策
- ピン留め: JDK 24 で
synchronizedは解消。JNI/ネイティブは避ける。 - ThreadLocal: ❌ VT では(メモリ爆発)。スコープ値(JDK 25 プレビュー)を使用。
- メトリクス/モニタリング:
Thread.currentThread().isVirtual();JFR は JDK 21 から VT サポート。 - フレームワーク: Spring Boot 3.2+、Quarkus、Helidon ネイティブ。
ベストプラクティス(2025):
- Web サーバでデフォルト VT 使用。
- JDK Mission Control/JFR でピン留め検出。
ThreadLocalをスコープ値に移行。- CPU 集約は固定プール。
- wrk/ab で >100k 同時テスト。
最終まとめ
| レベル | VT 使用? |
|---|---|
| 初心者 | はい、Thread.startVirtualThread() ですべての並行処理。 |
| 専門家 | I/O ははい;純粋 CPU はいいえ。ピン留めを監視。 |
メタファー: PT = 専用エレベータ(高価)。VT = 空気圧パイプシステム:少数の物理パイプで数千の軽量カプセル。
出典:OpenJDK JEPs、Spring Boot ベンチマーク(Reddit/Java 24)、RockTheJVM/RabinaNPatra ガイド(2025)。