#chiroito ’s blog

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

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

個人的に気になっている Project Loom で Virtual Threads がプレビューされたので仕様である JEP を翻訳してみました。 ボリュームが多いため、いくつかのパートに分けて公開していきます。

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

全パートはこちら

他の案

  • 非同期APIに依存し続けます。非同期APIは同期APIとの統合が難しく、同じI/O操作の2つの表現という分断された世界を作り出し、トラブルシューティング、監視、デバッグ、プロファイリングの目的でプラットフォームが状況に応じて使用できる、一連の操作に対する統一した概念を提供しません。
  • Java言語に構文的なスタックレス・コルーチン(すなわちasync/await)を追加することです。これらはユーザモードスレッドよりも実装が簡単で、一連の操作の状況を表す統一的な構造を提供するでしょう。しかし、この構成は新しく、スレッドとは別物で、多くの点でスレッドと似ていますが、ニュアンス的には異なるものです。スレッド用に設計されたAPIとコルーチン用に設計されたAPIの間で世界を二分し、プラットフォームとそのツールのすべてのレイヤーに新しいスレッドのような構造を導入する必要があるのです。これは、エコシステムが採用するのに時間がかかり、ユーザーモードのスレッドのようにエレガントでプラットフォームと調和したものにはならないでしょう。 構文的コルーチンを採用しているほとんどの言語は、ユーザーモードスレッドを実装できない(例:Kotlin)、従来のセマンティックの保証(例:本質的にシングルスレッドなJavaScript)、または言語固有の技術的制約(例:C++)のためにそうしてきました。これらの制約は、Javaには当てはまりません。
  • java.lang.Threadとは関係なく、ユーザモードのスレッドを表す新しいパブリッククラスを導入します。これは、Threadクラスが25年以上にわたって蓄積してきた不要なお荷物を捨てる機会になるでしょう。私たちは、このアプローチのいくつかのバリエーションを検討し、プロトタイプを作成しましたが、どの場合も、既存のコードをどのように実行するかという問題に直面することになりました。主な問題は、既存のコードで Thread.currentThread() が直接的または間接的に広く使用されていることです(例えば、ロック所有権の決定やスレッドローカル変数など)。このメソッドは、現在の実行スレッドを表すオブジェクトを返さなければなりません。もしユーザモードスレッドを表す新しいクラスを導入した場合、 currentThread() は Thread のように見えるがユーザモードスレッドオブジェクトに委ねるある種のラッパーオブジェクトを返さなければならないでしょう。現在の実行スレッドを表すオブジェクトが2つあると混乱するので、結局、古いThread APIを維持することは大きなハードルではないと判断しました。currentThread()などの一部のメソッドを除いて、開発者がThread APIを直接使うことはほとんどなく、ExecutorServiceなどの上位のAPIを使ってやり取りすることがほとんどです。Thread クラスや、ThreadGroup などの関連クラスから、不要なメソッドを非推奨にしたり削除したりすることで、時間をかけて不要な荷物を取り除いていく予定です。

テスト

  • 既存のテストは、私たちがここで提案する変更が、多数の構成と実行モードにおいて、予期せぬ回帰を引き起こさないことを保証するものです。
  • 私たちは jtreg テストツールを拡張し、既存のテストを仮想スレッドのコンテキストで実行できるようにする予定です。これにより、多くのテストで2つのバージョンを用意する必要がなくなります。
  • 新しいテストでは、すべての新しいAPIと改良されたAPI、および仮想スレッドをサポートするために変更されたすべての領域を検証します。
  • 新しい負荷テストは、信頼性と性能に重要な領域を対象とします。
  • 新しいマイクロベンチマークは、パフォーマンスが重要な部分をターゲットにします。
  • Helidon や Jetty などの既存のサーバーを大規模なテストに使用します。

リスクと前提条件

本提案の主なリスクは、既存のAPIやその実装の変更に伴う互換性の問題です。

  • java.io.BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, PrintStream, および PrintWriter クラスで使用される内部の(および文書化されていない)ロックプロトコルの改訂は、入出力メソッドが呼び出されたストリーム上で同期することを想定しているコードに影響を与えるかもしれません。この変更は、これらのクラスを継承し、スーパークラスによるロックを前提としているコードには影響を与えません。また、java.io.Reader または java.io.Writer を継承し、それらの API によって公開されるロック・オブジェクトを使用するコードにも影響しません。
  • java.lang.ThreadGroup は、スレッドグループを破棄できなくなりました。また、デーモンスレッドグループの概念をサポートしなくなり、その suspend(), resume(), stop() メソッドは常に例外をスローするようになりました。

プラットフォームスレッドと仮想スレッドの間には、既存のコードと仮想スレッドや新しいAPIを利用する新しいコードを組み合わせたときに、いくつかの動作の違いが見られることがあります。

  • Thread.setPriority(int) メソッドは、常に Thread.NORM_PRIORITY の優先度を持つ仮想スレッドには効果がありません。
  • Thread.setDaemon(boolean) メソッドは、常にデーモンスレッドである仮想スレッドに対して何の効果もありません。
  • Thread.stop()、suspend()、resume()メソッドは、仮想スレッド上で起動されるとUnsupportedOperationExceptionをスローします。
  • Thread API は、スレッドローカル変数をサポートしないスレッドの作成をサポートします。 ThreadLocal.set(T) および Thread.setContextClassLoader(ClassLoader) は、スレッドローカルをサポートしないスレッドのコンテキストで起動すると、UnsupportedOperationException をスローします。
  • Thread.getAllStackTraces() は、すべてのスレッドの Map ではなく、すべてのプラットフォームス レッドの Map を返すようになりました。
  • java.net.Socket, ServerSocket, DatagramSocket によって定義されたブロッキング I/O メソッドは、仮想スレッドのコンテキストで呼び出された場合、割り込み可能になりました。ソケット操作でブロックされているスレッドが割り込まれた場合、既存のコードが壊れる可能性がありました。この場合、スレッドを起動し、ソケットをクローズします。
  • 仮想スレッドは ThreadGroup のアクティブなメンバではありません。仮想スレッドで Thread.getThreadGroup() を呼び出すと、空のダミー "VirtualThreads" グループが返されます。
  • SecurityManagerが設定された状態で実行されている場合、仮想スレッドには権限はありません。
  • JVM TIでは、GetAllThreadsおよびGetAllStackTraces関数は、仮想スレッドを返しません。ThreadStartイベントとThreadEndイベントが有効な既存のエージェントは、イベントをプラットフォームスレッドに制限する機能がないため、パフォーマンスの問題が発生することも考えられます。
  • java.lang.management.ThreadMXBean APIは、プラットフォームスレッドの監視と管理をサポートしますが、仮想スレッドはサポートしません。
  • -XX:+PreserveFramePointerフラグは、仮想スレッドのパフォーマンスに重大な悪影響を及ぼします。

依存関係

  • JDK 18のJEP 416 (Reimplement Core Reflection with Method Handles)では、VM-native Reflectionの実装が削除されました。これにより、メソッドがリフレクションで呼び出されたときに、仮想スレッドがうまく一時停止するようになりました。
  • JDK 13 の JEP 353 (Reimplement the Legacy Socket API) と JDK 15 の JEP 373 (Reimplement the Legacy DatagramSocket API) は、java.net.Socket、ServerSocket、DatagramSocket の実装を仮想スレッドでの使用に向けて設計した新しい実装と置き換えたものです。
  • JDK 18 の JEP 418 (Internet-Address Resolution SPI) は、ホスト名とアドレス探索のためのサービスプロバイダインターフェースを定義しました。これにより、サードパーティーライブラリーは、ホスト探索中にスレッドを固定化しない代替の java.net.InetAddress 解決手段を実装できます。