#chiroito ’s blog

Java を中心とした趣味の技術について

日本語訳:JEP 425 : Virtual Threads (Preview) Part 2

個人的に気になっている Project Loom で Virtual Threads がプレビューされたので仕様である JEP を翻訳してみました。 ボリュームが多いため、いくつかのパートに分けて公開していきます。スターやブコメが付くとモチベーションが上がるかもしれません。(上がらないかもしれません)

原文はこちら:JEP 425: Virtual Threads (Preview)

全パートはこちら

説明

今日、JDKのjava.lang.Threadのすべてのインスタンスは、プラットフォームスレッドです。プラットフォームスレッドは、基礎となるOSスレッド上でJavaコードを実行し、コードの全生涯にわたってOSスレッドを捕捉します。プラットフォームスレッドの数は、OSスレッドの数に制限されています。

仮想スレッドは、基盤となるOSスレッド上でJavaコードを実行するjava.lang.Threadのインスタンスですが、コードの全生涯にわたってOSスレッドを捕捉することはありません。つまり、多くの仮想スレッドが同じOSスレッド上でJavaコードを実行することができ、事実上それを共有できます。プラットフォームスレッドが貴重なOSスレッドを独占するのに対して、仮想スレッドはそうではありません。仮想スレッドの数は、OSスレッドの数よりはるかに大きくできます。

仮想スレッドは、OSではなくJDKによって提供されるスレッドの軽量な実装です。これはユーザーモードスレッドの一種で、他のマルチスレッド言語(GoのゴルーチンやErlangのプロセスなど)でも成功したものです。ユーザーモードスレッドは、OSスレッドがまだ成熟しておらず普及していなかったJavaの初期バージョンでは、いわゆる「グリーンスレッド」として機能したこともあります。しかし、Javaのグリーンスレッドはすべて1つのOSスレッドを共有し(M:1スケジューリング)、最終的にはOSスレッドのラッパーとして実装されたプラットフォームスレッドに負けました(1:1スケジューリング)。仮想スレッドはM:Nスケジューリングを採用しており、多数の(M)個の仮想スレッドが少数の(N)個のOSスレッド上で動作するようにスケジューリングされます。

仮想スレッドとプラットフォームスレッドの使い分け

開発者は、仮想スレッドとプラットフォームスレッドのどちらを使うかを選択できます。ここでは、大量の仮想スレッドを作成するサンプルプログラムを紹介します。このプログラムは、まずExecutorServiceを取得し、提出された各タスクに対して新しい仮想スレッドを作成します。そして、10,000個のタスクを投入し、すべてのタスクが完了するのを待ちます。

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}  // executor.close()は暗黙のうちに呼び出され、待機します。

この例のタスクは、1秒間スリープするという単純なコードで、最近のハードウェアは、このようなコードを同時に実行する1万個の仮想スレッドを簡単にサポートできます。裏側では、JDKは少数のOSスレッド(おそらく1スレッド程度)でコードを実行しています。

このプログラムが、Executors.newCachedThreadPool()のような、各タスクに対して新しいプラットフォームスレッドを作成するExecutorServiceを使用していたら、状況は大きく変わっていたことでしょう。ExecutorService は 10,000 個のプラットフォームスレッドを作成しようとするので、OS スレッドも 10,000 個になり、マシンとオペレーティングシステムによってはプログラムがクラッシュする可能性があります。

もしこのプログラムが、Executors.newFixedThreadPool(200)のようなプールからプラットフォームスレッドを取得するExecutorServiceを使用するなら、状況はあまり良くならないでしょう。このExecutorServiceは、1万個のタスクが共有する200個のプラットフォームスレッドを作成するため、多くのタスクが同時ではなく連続して実行され、プログラムの完了に長い時間がかかることになります。このプログラムでは、200個のプラットフォームスレッドを持つプールでは、200タスク/秒のスループットしか達成できませんが、仮想スレッドでは、(十分なウォームアップの後)約10,000タスク/秒のスループットが達成されます。さらに、このプログラムの10_000を1_000_000に変更すると、100万個のタスクを投入し、100万個の仮想スレッドを生成して同時実行し、(十分なウォームアップの後)約100万タスク/秒のスループットを達成できます。

このプログラムのタスクが、単にスリープするのではなく、1秒間計算を行う(例えば、巨大な配列をソートする)場合、プロセッサコアの数以上にスレッドの数を増やしても、それが仮想スレッドであろうとプラットフォームスレッドであろうと効果はありません。仮想スレッドは高速なスレッドではなく、プラットフォームスレッドよりも高速にコードを実行するわけではありません。仮想スレッドは高速化(低レイテンシ)ではなく、スケール(高スループット)を実現するために存在します。仮想スレッドはプラットフォームスレッドよりも多く存在するため、リトルの法則に従って、高いスループットに必要な高い並行処理が可能になります。

別の言い方をすれば、仮想スレッドは次のような場合にアプリケーションのスループットを大幅に向上させます。

  • 同時実行タスク数が多い(数千以上)。
  • 作業負荷が CPU 拘束でないこと。この場合、プロセッサコア数よりも多くのスレッドがあっても、スループットを向上させることはできないからです。

仮想スレッドは、一般的なサーバーアプリケーションのスループットを向上させるのに役立ちます。なぜなら、そのようなアプリケーションは、多くの時間を待機して過ごす多数の同時実行タスクで構成されているからです。

仮想スレッドは、プラットフォームスレッドが実行可能なあらゆるコードを実行できます。特に、仮想スレッドは、プラットフォームスレッドと同様に、スレッドローカル変数とスレッド割り込みをサポートします。つまり、リクエストを処理する既存の Java コードは、簡単に仮想スレッドで実行できるのです。多くのサーバーフレームワークはこれを自動的に行い、入ってくるリクエストごとに新しい仮想スレッドを開始し、その中でアプリケーションのビジネスロジックを実行することを選択するでしょう。

以下は、他の2つのサービスの結果を集約するサーバーアプリケーションの例です。架空のサーバーフレームワーク(示されていない)は、各リクエストに対して新しい仮想スレッドを作成し、その仮想スレッドでアプリケーションのハンドルコードを実行します。一方、アプリケーションのコードは、最初の例と同じ ExecutorService を介してリソースを同時に取得するために、2 つの新しい仮想スレッドを作成します。

void handle(Request request, Response response) {
    var url1 = ...
    var url2 = ...
 
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var future1 = executor.submit(() -> fetchURL(url1));
        var future2 = executor.submit(() -> fetchURL(url2));
        response.send(future1.get() + future2.get());
    } catch (ExecutionException | InterruptedException e) {
        response.fail(e);
    }
}
 
String fetchURL(URL url) throws IOException {
    try (var in = url.openStream()) {
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    }
}

このようなサーバーアプリケーションでは、素直なブロッキングコードを使用することで、多数の仮想スレッドを使用できるため、スケールが良くなります。

Executor.newVirtualThreadPerTaskExecutor() は、仮想スレッドを作成する唯一の方法というわけではありません。後述する新しい java.lang.Thread.Builder API は、仮想スレッドを作成し、開始できます。さらに、構造化された同時実行は、特にこのサーバの例のようなコードにおいて、仮想スレッドを作成し管理する、より強力なAPIを提供し、それによってスレッド間の関係がプラットフォームとそのツールに知られるようになるのです。

仮想スレッドはプレビュー用APIで、デフォルトでは無効

上記のプログラムは Executors.newVirtualThreadPerTaskExecutor() メソッドを使用しているので、JDK 19 で実行するには、以下のようにプレビュー API を有効にする必要があります。

  • javac --release 19 --enable-preview Main.java でコンパイルし、java --enable-preview Main で実行します。
  • または、ソースコードランチャーを使用する場合は、java --source 19 --enable-preview Main.java でプログラムを実行します。
  • または、jshellを使用する場合は、jshell --enable-previewで起動します。

仮想スレッドをプールしない

開発者は通常、アプリケーションコードを、スレッドプールに基づく従来の ExecutorService から、リクエスト毎の仮想スレッド ExecutorService に移行することになります。スレッドプールは、他のリソースプールと同様に、高価なリソースを共有するためのものですが、仮想スレッドは高価ではないので、プールする必要はありません。

開発者は、限られたリソースへの同時アクセスを制限するために、スレッドプールを使用することがあります。例えば、あるサービスが20以上の同時リクエストを処理できない場合、サイズ20のプールに投入されたタスクを経由してサービスへのすべてのアクセスを実行することで、それを確実にできます。プラットフォームスレッドのコストが高いため、スレッドプールが一般的になり、このイディオムも一般的になりました。しかし、開発者は並行処理を制限するために仮想スレッドをプールする誘惑に駆られるべきではありません。限られたリソースへのアクセスを保護するために、セマフォのようなその目的のために特別に設計された構造を使用する必要があります。これはスレッドプールよりも効果的で便利です。また、スレッドローカルデータがあるタスクから別のタスクに誤って漏れる危険性がないため、より安全です。

仮想スレッドの監視

明確なコードを書くことが全てではありません。実行中のプログラムの状態を明確に示すことは、トラブルシューティング、メンテナンス、最適化にも不可欠であり、JDKは長い間、スレッドのデバッグ、プロファイル、監視のためのメカニズムを提供してきました。このようなツールは、おそらくは、その膨大な量に多少の配慮をした上で仮想スレッドに対しても同じことを行うべきです。おそらく、仮想スレッドは結局のところ、java.lang.Threadのインスタンスなのです。

Javaデバッガは、仮想スレッドのステップ実行、コールスタックの表示、スタックフレーム内の変数の検査が可能です。JDK Flight Recorder (JFR) は、JDK の低オーバーヘッドのプロファイリングとモニタリングのメカニズムで、アプリケーションコードからのイベント(オブジェクトの割り当てや I/O 操作など)を正しい仮想スレッドに関連付けられます。これらのツールは、非同期スタイルで書かれたアプリケーションに対してこれらのことを行えません。このスタイルでは、タスクはスレッドに関連しないため、デバッガはタスクの状態を表示したり操作したりできず、プロファイラもタスクがI/O待ちをしている時間を知れないのです。

スレッドダンプは、リクエスト毎のスレッドで書かれたアプリケーションのトラブルシューティ ングによく使われるツールのひとつです。残念ながら、JDKの従来のスレッドダンプは、jstackやjcmdで得られる、スレッドのフラットなリストを提示するものである。これは、数十または数百のプラットフォームスレッドには適していますが、数千または数百万の仮想スレッドには不向きです。したがって、従来のスレッドダンプを拡張して仮想スレッドを含めるのではなく、jcmdに新しい種類のスレッドダンプを導入して、仮想スレッドをプラットフォームスレッドと一緒に、すべて意味のある方法でグループ化して表示することにします。プログラムが構造化された並行処理を使用する場合、スレッド間のより豊かな関係を表示できます。

多くのスレッドを可視化し、分析することは、ツールの恩恵を受けるので、jcmdは、プレーンテキストに加えて、JSONフォーマットで新しいスレッドダンプを出力できます。

$ jcmd <pid> Thread.dump_to_file -format=json <file>

新しいスレッドダンプ形式は、ネットワークI/O操作でブロックされた仮想スレッドと、上に示した新しいリクエスト毎のExecutorServiceで作成された仮想スレッドを列挙します。オブジェクトアドレス、ロック、JNI統計、ヒープ統計など、従来のスレッドダンプに表示される情報は含まれません。さらに、非常に多くのスレッドをリストアップする必要があるため、新しいスレッドダンプを生成しても、アプリケーションを一時停止することはありません。

このようなスレッドダンプの例を、上記の2番目の例と同様のアプリケーションから取得し、JSONビューアでレンダリングしたものを示します。

仮想スレッドはJDKに実装され、特定のOSスレッドと関連付けられていないため、OSからは不可視であり、OSはその存在に気付きません。OSレベルの監視は、JDKプロセスが使用するOSスレッドの数が仮想スレッドの数より少ないことを観察します。

続きはこちら: 日本語訳:JEP 425 : Virtual Threads (Preview) Part 3 - #chiroito ’s blog

Infinispan / Red Hat Data Grid を監視する

Infinispan (Red Hat Data Grid) が提供している監視に使える機能についてまとめました。環境に合わせて適宜設定することをお勧めします。

この記事では Infinispan および Red Hat Data Grid のインストール先のディレクトリをINFINISPAN_HOMEとしています。

ログ出力

出力したいログの設定は${INFINISPAN_HOME}/server/conf/log4j2.xmlで定義します。設定ファイルの名前の通り、設定はLog4j 2の形式で設定します。

ログはデフォルトで{INFINISPAN_HOME}/server/logに出力されます。

メトリクス

Infinispan は OpenMetrics 形式でメトリクスを提供しています。このメトリクスはHTTPでアクセスできます。そのため、このURLを指定することでさまざまな監視ツールからメトリクスを収集できます。

Infinispan のメトリクスは JMX を使用して MBean にアクセスすることでも取得できますが、今の時代では使われることが少ないので省略します。必要な場合は参考資料をご覧ください。

メトリクスを有効にするには、${INFINISPAN_HOME}/server/conf/infinispan.xmlなどのInfinispanの設定ファイルでcache-container要素のstatistics属性にtrueを設定します。

<cache-container name="default" statistics="true"></cache-container>

この設定をしてInfinispanを起動するとHTTPでメトリクスが公開されます。

URL は以下の通りです。

http://<ホスト名>:<ポート番号>/metrics/

以下のようなメトリクスが出力されます。

# HELP base_memory_committedNonHeap_bytes Displays the amount of memory that is committed for the Java virtual machine to use.
# TYPE base_memory_committedNonHeap_bytes gauge
base_memory_committedNonHeap_bytes 9.5879168E7
# HELP base_memory_maxHeap_bytes Displays the maximum amount of memory in bytes that can be used for memory management.
# TYPE base_memory_maxHeap_bytes gauge
base_memory_maxHeap_bytes 1.70917888E10
# HELP base_gc_time_total Displays the approximate accumulated collection elapsed time in milliseconds. This attribute displays -1 if the collection elapsed time is undefined for this collector. The Java virtual machine implementation may use a high resolution timer to measure the elapsed time. This attribute may display the same value even if the collection count has been incremented if the collection elapsed time is very short.
# TYPE base_gc_time_total counter
base_gc_time_total_seconds{name="G1 Young Generation1"} 0.037
(略)

ヘルス

Infinispanはクラスタの状態とキャッシュマネージャの状態を REST API として提供しています。

クラスタの状態

クラスタの状態を取得するには以下の URL へアクセスします。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/<キャッシュマネージャ名>/health

URL にはキャッシュマネージャ名を付与します。

キャッシュマネージャ名はデフォルトではdefaultになります。この場合、以下のURLになります。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/default/health

出力される結果は以下の通りです。

{
  "cluster_health":{
    "cluster_name":"cluster",
    "health_status":"HEALTHY",
    "number_of_nodes":1,
    "node_names":["Development-59678"]
  },
  "cache_health":[
    {"status":"HEALTHY","cache_name":"javaChildCache"},
    {"status":"HEALTHY","cache_name":"___protobuf_metadata"},
    {"status":"HEALTHY","cache_name":"protoBufChildUngroupCache"},
    {"status":"HEALTHY","cache_name":"javaParentCache"},
    {"status":"HEALTHY","cache_name":"protoBufParentUngroupCache"},
    {"status":"HEALTHY","cache_name":"protoBufParentCache"},
    {"status":"HEALTHY","cache_name":"protoBufChildCache"},
    {"status":"HEALTHY","cache_name":"___script_cache"}
  ]
}

キャッシュマネージャの状態

キャッシュマネージャの状態を取得するには以下の URL へアクセスします。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/<キャッシュマネージャ名>/health/status

URL にはキャッシュマネージャ名を付与します。

キャッシュマネージャ名はデフォルトではdefaultになります。この場合、以下のURLになります。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/default/health/status

出力される結果は以下の通りです。

HEALTHY

JDK Flight Recorder

Java 11以降もしくはJava 8の場合はupdate 272以降の Java を使っている場合は JDK Flight Recorder を使いましょう。

Javaの実行時VM引数に-XX:StartFlightRecording=name=DefaultRecording,settings=default,disk=true,filename=DefaultRecording.jfr,dumponexit=trueを追加してください。

シェルから起動している場合

シェルから起動している場合は、${INFINISPAN_HOME}/bin/server.shもしくは${INFINISPAN_HOME}/bin/server.bat の最後 java コマンドを実行しているところに追加します。

server.sh

   eval \"$JAVA\" $JAVA_OPTS \
      -Dvisualvm.display.name=$PROCESS_NAME \
      -XX:StartFlightRecording=name=DefaultRecording,settings=default,disk=true,filename=DefaultRecording.jfr,dumponexit=true \
      -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \

server.bat

"%JAVA%" %JAVA_OPTS%  -showversion ^
   -Dvisualvm.display.name=%PROCESS_NAME% ^
   -XX:StartFlightRecording=name=DefaultRecording,settings=default,disk=true,filename=DefaultRecording.jfr,dumponexit=true ^
   "-Dinfinispan.server.home.path=%ISPN_HOME%" ^
(略)

参考資料

日本語訳:JEP 425 : Virtual Threads (Preview) Part 1

個人的に気になっている Project Loom で Virtual Threads がプレビューされたので仕様である JEP を翻訳してみました。 ボリュームが多いため、いくつかのパートに分けて公開していきます。スターやブコメが付くとモチベーションが上がるかもしれません。(上がらないかもしれません)

原文はこちら:JEP 425: Virtual Threads (Preview)

全パートはこちら

JEP 425 : Virtual Threads (Preview)

  • 著者:Ron Pressler, Alan Bateman
  • オーナー:Alan Bateman
  • タイプ:機能
  • スコープ:SE
  • ステータス:統合済み
  • リリース:19
  • コンポーネント:core-libs
  • 議論:loom dash dev at openjdk dot java dot net
  • 労力:XL
  • レビュワ:Alex Buckley, Brian Goetz, Chris Hegarty
  • 作成日:2021/11/15 16:43
  • 更新日:2022/05/21 16:58
  • イシュー:8277131

要約

Java プラットフォームに仮想スレッドを導入します。仮想スレッドは、高スループットの並列アプリケーションを書き、維持し、観察する労力を劇的に削減する軽量なスレッドです。このAPIはプレビュー版です。

ゴール

  • シンプルなスレッド単位でリクエストするスタイルで書かれたサーバアプリケーションを、ほぼ最適なハードウェア使用率でスケーリングできるようにします。
  • java.lang.Thread API を使用する既存のコードに、最小限の変更で仮想スレッドを採用できるようにします。
  • 既存の JDK ツールを使用して、仮想スレッドのトラブルシューティング、デバッグ、およびプロファイリングを容易に行えるようにします。

非ゴール

  • 従来のスレッドの実装を削除したり、既存のアプリケーションを仮想スレッドを使用するようにユーザに明示せずに移行させることが目的ではありません。
  • Javaの基本的な並行処理モデルを変更することが目的ではありません。
  • Java言語やJavaライブラリに新しいデータ並列化構造を提供することも目的ではありません。Stream APIは、大規模なデータセットを並列に処理するための好ましい方法であることに変わりはありません。

動機

Java開発者は、30年近くにわたり、並行処理サーバーアプリケーションの構成要素としてスレッドに依存してきました。Javaはマルチスレッドなので、複数のスレッドが一度に実行され、各メソッドの全ての処理はスレッドの中で実行されます。スレッドは、Javaの並行処理の単位であり、他の単位と同時に(そしてほとんど独立して)実行される逐次実行されるコードの一部分です。各スレッドは、ローカル変数を格納し、メソッド呼び出しを調整するためのスタックと、物事がうまくいかないときのコンテキストを提供します。例外は、同じスレッドのメソッドによってスローおよびキャッチされるので、開発者はスレッドのスタックトレースを使用して、何が起こったかを見つけられます。スレッドは、ツールの中心的な概念でもあります。デバッガはスレッドのメソッド内のステートメントをステップ実行し、プロファイラは複数のスレッドの挙動を可視化し、そのパフォーマンスを理解するのに役立ちます。

リクエスト毎にスレッドを使う方式

サーバーアプリケーションは一般に、互いに独立したユーザーリクエストを同時に処理するため、アプリケーションがリクエストの全期間にわたってそのリクエストにスレッドを割り当てて処理することは理にかなっています。このスレッド毎のリクエスト方式は、アプリケーションの並行性の単位を表すためにプラットフォームの並行性の単位を使用するので、理解しやすく、プログラミングしやすく、デバッグやプロファイルも簡単です。

サーバーアプリケーションの拡張性は,待ち時間,同時実行性,スループット を関連付けるリトルの法則によって支配されています。あるリクエスト処理時間(すなわち待ち時間)において、アプリケーションが同時に処理するリクエストの数(すなわち同時実行性)は、到着率(すなわちスループット)に比例して増加しなければなりません。たとえば、平均待ち時間が50msのアプリケーションが、10リクエストを同時に処理することによって、1秒間に200リクエストのスループットを達成したとします。そのアプリケーションが 1 秒あたり 2000 リクエストのスループットに拡張するためには、100 リクエストを同時に処理する必要があります。各リクエストがリクエストの間、スレッドで処理される場合、アプリケーションが維持するために、スループットが増加するにつれてスレッドの数は増加する必要があります。

残念ながら、JDKはスレッドをオペレーティングシステム(OS)スレッドのラッパーとして実装しているため、利用可能なスレッドの数は限られています。OSのスレッドはコストが高いので、あまり多く持つことができず、リクエスト毎のスレッドというスタイルには不向きな実装になっています。各リクエストがスレッド、つまりOSのスレッドを消費する場合、CPUやネットワーク接続などの他のリソースが枯渇する前に、スレッドの数が制限要因になることが多いのです。JDKの現在のスレッド実装は、アプリケーションのスループットを、ハードウェアがサポートできるレベルよりもかなり低い水準に制限しています。これは、スレッドがプールされている場合でも発生します。プールすることで、新しいスレッドを開始する際の高いコストを回避できますが、スレッドの総数が増えるわけではありません。

非同期式でスケーラビリティを向上

ハードウェアを最大限に活用したい開発者の中には、リクエスト毎のスレッドをあきらめ、スレッドを共有するスタイルを採用する人もいます。1つのスレッドで最初から最後までリクエストを処理するのではなく、リクエスト処理コードは、I/O処理が完了するのを待ってスレッドをプールに戻し、そのスレッドが他のリクエストに対応できるようにします。このようにスレッドをきめ細かく共有することで、コードが計算を行うときだけスレッドを保持し、I/Oを待つときには保持しないことで、多くのスレッドを消費せずに多くの同時処理を行うことができるのです。OSのスレッドの不足によるスループットの制限をなくす一方で、高い代償を払うことになります。それは、いわゆる非同期プログラミングスタイルで、I/O操作の完了を待たず、後でコールバックに完了を知らせるI/Oメソッドを別に採用する必要があることです。専用のスレッドがない場合、開発者はリクエスト処理ロジックを小さな段階に分解し、通常はラムダ式で記述し、APIを使って順次パイプラインに合成しなければなりません(例えば、CompletableFutureや、いわゆる「リアクティブ」フレームワークなどを参照ください)。そのため、ループやtry/catchブロックといった、言語の基本的な逐次合成演算子を使いません。

非同期式では、リクエストの各ステージは異なるスレッドで実行され、各スレッドは異なるリクエストに属するステージを相互に切り離して実行することがあります。このことは、プログラムの動作を理解する上で深い意味を持ちます。スタックトレースは有用なコンテキストを提供せず、デバッガはリクエスト処理ロジックを段階的に処理できず、プロファイラは操作のコストをその呼び出し元と関連付けることができません。ラムダ式の記述は、JavaのストリームAPIを使って短いパイプラインでデータを処理する場合には何とかなりますが、アプリケーションのリクエスト処理コードをすべてこの方法で書かなければならない場合には問題があります。なぜなら、アプリケーションの並行性の単位である非同期パイプラインは、もはやプラットフォームの並行性の単位ではなくなっているからです。

仮想スレッドによるリクエスト毎のスレッド方式の維持

アプリケーションをプラットフォームと調和させながら拡張するためには、スレッドをより効率的に実装することで、リクエスト毎のスレッドというスタイルを維持し、より多くのスレッドを使えるようにする努力が必要です。言語やランタイムによってスレッドスタックの使い方が異なるため、OSのスレッドをより効率的に実装できません。しかし、Javaランタイムは、OSのスレッドとの一対一の対応を絶つ方法でJavaのスレッドを実装することができます。OSが限られた物理RAMに大きな仮想アドレス空間をマッピングすることでメモリが豊富にあるように見せかけるのと同じように、Javaランタイムも少数のOSスレッドに多数の仮想スレッドをマッピングすることでスレッドが豊富にあるように見せかけることができるのです。

仮想スレッドは、特定のOSスレッドに縛られないjava.lang.Threadのインスタンスです。これに対してプラットフォームスレッドは、OSのスレッドの薄いラッパーとして、伝統的な方法で実装されたjava.lang.Threadのインスタンスです。

リクエスト毎のスレッド方式のアプリケーションコードは、リクエストの全期間にわたって仮想スレッドで実行できますが、仮想スレッドは、CPUで計算を実行する間のみOSスレッドを消費します。その結果、非同期スタイルと同じスケーラビリティを、透過的に達成できます。仮想スレッドで実行されているコードがjava.* APIでブロッキングI/Oオペレーションを呼び出すと、ランタイムはノンブロッキングOSコールを実行し、後で再開できるまで仮想スレッドを自動的にサスペンドさせるのです。Java開発者にとっては、仮想スレッドは単にスレッドの作成が安上がりで、ほとんど無限にあるようなものです。ハードウェアの使用率は最適に近く、高いレベルの並行処理が可能になり、その結果高いスループットが得られる一方、アプリケーションはJavaプラットフォームとそのツールのマルチスレッド設計と調和したままです。

仮想スレッドの意味するところ

仮想スレッドは安価で豊富なため、決してプールしてはいけません。アプリケーション・タスクごとに新しい仮想スレッドを作成する必要があります。ほとんどの仮想スレッドは短命でコールスタックも浅く、1回のHTTPクライアントコールや1回のJDBCクエリを実行する程度です。対照的に、プラットフォームスレッドは重量級で高価であるため、しばしばプールする必要があります。スレッドは長寿命で、コールスタックも深く、多くのタスクで共有される傾向があります。

要約すると、仮想スレッドは、ハードウェアを最適に利用しながら、Java プラットフォームの設計と調和した信頼性の高いリクエスト毎のスレッド方式を維持します。仮想スレッドを使うのに新しい概念を学ぶ必要はありませんが、今日の高いスレッド費用に対処するために開発された習慣を学ぶ必要はあるかもしれません。仮想スレッドは、アプリケーション開発者を助けるだけでなく、フレームワーク設計者がスケーラビリティに妥協することなくプラットフォームの設計に適合した使いやすいAPIを提供するのにも役立ちます。

続きはこちら:日本語訳:JEP 425 : Virtual Threads (Preview) Part 2 - #chiroito ’s blog