Zademy

Java の仮想スレッド:スケーラビリティと複雑性のバランス

Java; 仮想スレッド; ProjectLoom; 並行性; JDK25
819 単語

従来の並行性の問題

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 コア)にマッピング。

  1. キャリア: VT を「運ぶ」プラットフォームスレッド。
  2. マウント/アンマウント: VT が実行のためにキャリアにマウント。I/O ブロックでアンマウント(sleep()、ソケット、DB クエリ)。
  3. JVM スケジューラ: 動的にキャリアを再割り当て。I/O 待機中はキャリアが解放。
  4. コンティニュエーション: 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):

  1. Web サーバでデフォルト VT 使用。
  2. JDK Mission Control/JFR でピン留め検出。
  3. ThreadLocal をスコープ値に移行。
  4. CPU 集約は固定プール。
  5. wrk/ab で >100k 同時テスト。

最終まとめ

レベルVT 使用?
初心者はい、Thread.startVirtualThread() ですべての並行処理。
専門家I/O ははい;純粋 CPU はいいえ。ピン留めを監視。

メタファー: PT = 専用エレベータ(高価)。VT = 空気圧パイプシステム:少数の物理パイプで数千の軽量カプセル。

出典:OpenJDK JEPs、Spring Boot ベンチマーク(Reddit/Java 24)、RockTheJVM/RabinaNPatra ガイド(2025)。