#chiroito ’s blog

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

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

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

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

全パートはこちら

詳細な変更点

残りの章では、Javaプラットフォームとその実装において、私たちが提案する変更点について詳しく説明します。

java.lang.Thread

java.lang.Thread APIを以下のように更新します。

  • Thread.Builder, Thread.ofVirtual(), Thread.ofPlatform() は,仮想スレッドとプラットフォームスレッドを生成するための新しいAPIです.例えば
Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);

は、"duke" という名前の新しい未起動の仮想スレッドを作成します。

  • Thread.startVirtualThread(Runnable) は、仮想スレッドを作成し、起動する便利な方法です。
  • Thread.Builder は、スレッドまたは ThreadFactory を作成し、同一のプロパティを持つ複数のスレッドを作成できます。
  • Thread.isVirtual() は、スレッドが仮想スレッドであるかどうかを判定します。
  • Thread.join と Thread.sleep の新しいオーバーロードは、待ち時間とスリープ時間を java.time.Duration のインスタンスとして受け取ります。
  • 新しい final メソッド Thread.threadId() は、スレッドの識別子を返します。既存の non-final メソッド Thread.getId() は、現在非推奨です。
  • Thread.getAllStackTraces() は、すべてのスレッドではなく、すべてのプラットフォームスレッドのマップを返すようになりました。

java.lang.Thread APIは、それ以外には変更ありません。Threadクラスで定義されたコンストラクタは、以前と同様にプラットフォームスレッドを作成します。新しいパブリック コンストラクタはありません。

仮想スレッドとプラットフォームスレッドの主なAPIの違いは以下のとおりです。

  • パブリックスレッドコンストラクタは、仮想スレッドを作成できません。
  • 仮想スレッドは常にデーモンスレッドです。Thread.setDaemon(boolean) メソッドは、仮想スレッドを非デーモンスレッドに変更できません。
  • 仮想スレッドは Thread.NORM_PRIORITY という固定の優先度を持ちます。Thread.setPriority(int) メソッドは、仮想スレッドに影響を与えません。この制限は、将来のリリースで再検討される可能性があります。
  • 仮想スレッドは、スレッドグループのアクティブなメンバーではありません。仮想スレッド上で呼び出されると、Thread.getThreadGroup() は "VirtualThreads" という名前のプレースホルダ・スレッド・グループを返します。Thread.Builder APIは、仮想スレッドのスレッドグループを設定するメソッドを定義していません。
  • 仮想スレッドは、SecurityManager が設定された状態で実行されている場合、パーミッションを持ちません。
  • 仮想スレッドは、stop()、suspend()、resume()メソッドをサポートしません。これらのメソッドは、仮想スレッド上で呼び出されると例外をスローします。

スレッドローカル変数

仮想スレッドは、プラットフォームスレッドと同様にスレッドローカル変数(ThreadLocal)と継承可能なスレッドローカル変数(InheritableThreadLocal)をサポートしているので、スレッドローカルを使用する既存のコードを実行できます。ただし、仮想スレッドは非常に数が多いため、スレッドローカルは慎重に検討した上で使用してください。特に、スレッドプールで同じスレッドを共有する複数のタスクの間で、コストのかかるリソースをプールするためにスレッドローカルを使用しないでください。仮想スレッドは、そのライフタイムを通じて単一のタスクのみを実行することを目的としているため、決してプールしてはいけません。私たちは、何百万ものスレッドで実行する際のメモリフットプリントを減らすために、仮想スレッドの準備のために、java.baseモジュールからスレッドローカルの多くの使用を削除しています。

さらに:

  • Thread.Builder API は、スレッド作成時にスレッドロカールを無効にするメソッドを定義しています。また、継承可能なスレッドローカルの初期値を継承しないようにするためのメソッドも定義しています。スレッドロカールをサポートしないスレッドから呼び出された場合、 ThreadLocal.get() は初期値を返し、 ThreadLocal.set(T) は例外をスローします。
  • 従来のコンテキスト・クラスローダーは、継承可能なスレッドローカルのように動作するように指定されています。Thread.setContextClassLoader(ClassLoader) がスレッドローカルをサポートしないスレッドで実行された場合、例外がスローされます。

スコープローカル変数は、スレッドローカルの代わりとして、より良い使用例であることが証明されるかもしれません。

java.util.concurrent

ロック機能をサポートするプリミティブ API である java.util.concurrent.LockSupport が、仮想スレッドをサポートするようになりました。仮想スレッドを一時停止すると、基盤となるプラットフォーム・スレッドが他の作業を行うために解放され、仮想スレッドの一時停止を解除すると、そのスレッドが続行するようにスケジュールされます。この LockSupport の変更により、これを使用するすべての API(ロック、セマフォ、ブロッキングキューなど)が、仮想スレッドで呼び出されたときに適切に一時停止するようになりました。

さらに:

  • Executors.newThreadPerTaskExecutor(ThreadFactory) および Executors.newVirtualThreadPerTaskExecutor() は、タスクごとに新しいスレッドを生成する ExecutorService を作成します。これらのメソッドにより、スレッドプールやExecutorServiceを使用する既存のコードとの移行や相互運用が可能になります。
  • ExecutorService が AutoCloseable を継承するようになったため、例に書いたように try-with-resource 構造でこの API を使用できるようになりました。
  • Futureは、完了したタスクの結果または例外を取得するメソッドと、タスクの状態を取得するメソッドを定義するようになりました。これらの追加を組み合わせると、Futureオブジェクトをストリームの要素として使うことが容易になります。Futureのストリームをフィルタリングして完了したタスクを見つけ、それをマッピングして結果のストリームを取得できます。これらのメソッドは、構造化された並行処理のために提案されたAPI追加に対しても有用です。

ネットワーク

java.net および java.nio.channels パッケージのネットワーク API の実装は、仮想スレッドで動作するようになりました。ネットワーク接続の確立やソケットからの読み取りなどのためにブロックされた仮想スレッド上の操作は、他の作業を行うために基盤となるプラットフォームスレッドを解放します。

割り込みやキャンセルができるように、java.net.Socket, ServerSocket, DatagramSocketで定義されているブロッキングI/Oメソッドは、仮想スレッドで起動されると割り込みできるように仕様変更されました。ソケット上でブロックされている仮想スレッドに割り込みをかけると、スレッドの停 止が解除され、ソケットが閉じられます。これらのタイプのソケットでInterruptibleChannelから取得したI/O操作をブロックすることは、常に割り込み可能でした。この変更により、これらのAPIのコンストラクタで作成されたときの動作と、チャネルから取得したときの動作が一致するようになりました。

java.io

java.ioパッケージは、バイトや文字のストリームのためのAPIを提供します。これらのAPIの実装は高度に同期化されており、仮想スレッドで使用する際には固定化を回避するための変更が必要です。

背景として、バイト指向の入出力ストリームはスレッドセーフであることが指定されておらず、スレッドが読み取りまたは書き込みメソッドでブロックされている間にclose()が呼び出された場合に期待される動作が指定されていません。ほとんどのシナリオにおいて、複数の並行スレッドから特定の入出力ストリームを使用することは意味をなさない。文字指向のリーダライタもスレッドセーフであることは指定されていませんが、サブクラスのためにロックオブジェクトを公開します。固定化は別として、これらのクラスでの同期には問題があり、一貫性がありません。 例えば、InputStreamReader と OutputStreamWriter が使用するストリームデコーダとエンコーダは、ロックオブジェクトではなく、ストリームオブジェクト上で同期を取ります。

固定化を防ぐために、現在では以下のような実装になっています。

  • BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, PrintStream, PrintWriter は、直接使用する場合、モニターではなく、明示的なロックを使用するようになりました。これらのクラスは、サブクラス化されると従来どおり同期化されます。
  • InputStreamReader および OutputStreamWriter によって使用されるストリーム・デコーダおよびエンコーダは、InputStreamReader または OutputStreamWriter が包含するものと同じロックを使用するようになりました。

さらに進んで、しばしば必要とされるロックをすべて排除することは、このJEPの範囲を超えています。

さらに、BufferedOutputStream、BufferedWriter、および OutputStreamWriter のストリームエンコーダーで使用されるバッファの初期サイズが小さくなり、ヒープに多くのストリームやライターがある場合のメモリ使用量を減らしました。このため、ヒープ内に多くのストリームやライターが存在する場合、メモリ使用量を減らすことができます。

Java Native Interface (JNI)

JNIは、オブジェクトが仮想スレッドであるかどうかをテストするための新しい関数、IsVirtualThreadを1つ定義しています。

それ以外のJNI仕様は変更されていません。

デバッグ

デバッグ・アーキテクチャは、3つのインターフェースから構成されています。JVM Tool Interface (JVM TI), Java Debug Wire Protocol (JDWP), そして Java Debug Interface (JDI) です。この3つのインタフェースはすべて仮想スレッドをサポートするようになりました。

JVM TIのアップデートは

  • jthread(すなわち、ThreadオブジェクトへのJNI参照)で呼び出されるほとんどの関数は、仮想スレッドへの参照で呼び出せます。少数の関数、すなわちPopFrame、ForceEarlyReturn、StopThread、AgentStartFunction、およびGetThreadCpuTimeは、仮想スレッドではサポートされていません。SetLocal* 関数は、ブレークポイントまたはシングルステップのイベントで中断された仮想スレッドの最上位フレームにローカル変数を設定する場合に限定されます。
  • GetAllThreads および GetAllStackTraces 関数は、すべてのスレッドではなく、すべてのプラットフォームスレッドを返すように指定されるようになりました。
  • 初期の VM スタートアップ中またはヒープ反復中に通知されるものを除いて、すべてのイベントは、仮想スレッドのコンテキストで呼び出されるイベントコールバックを持てるようになりました。
  • 中断/再開の実装により、デバッガによる仮想スレッドの中断と再開が可能になり、仮想スレッドがマウントされたときにプラットフォームスレッドを中断できるようになりました。
  • 新しい機能である can_support_virtual_threads は、エージェントが仮想スレッドの開始と終了のイベントをより細かく制御できるようにします。
  • 新しい関数は仮想スレッドの一括停止と再開をサポートします。これらは can_support_virtual_threads 機能を必要とします。

既存のJVM TIエージェントは、ほとんど以前のように動作しますが、仮想スレッドでサポートされていない関数を呼び出すと、エラーが発生する可能性があります。これは、仮想スレッドを認識しないエージェントが、仮想スレッドを使用するアプリケーションで使用される場合に発生します。プラットフォームスレッドのみを含む配列を返すように GetAllThreads を変更したことは、一部のエージェントにとって問題となるかもしれません。ThreadStart および ThreadEnd イベントを有効にしている既存のエージェントは、これらのイベントをプラットフォームスレッドに制限する機能がないため、パフォーマンスの問題が発生する可能性があります。

JDWPのアップデートは

  • 新しいコマンドにより、デバッガはスレッドが仮想スレッドであるかどうかを確認できるようになりました。
  • EventRequestコマンドの新しい識別子により、デバッガはスレッドの開始と終了イベントをプラットフォームスレッドに制限できるようになりました。

JDIのアップデートは

  • com.sun.jdi.ThreadReference の新しいメソッドは、スレッドが仮想スレッドであるかどうかを検査します。
  • com.sun.jdi.request.ThreadStartRequest および com.sun.jdi.request.ThreadDeathRequest の新しいメソッドは、リクエストに対して発生するイベントをプラットフォームスレッドに制限しています。

上記のように、仮想スレッドはスレッドグループ内のアクティブスレッドとはみなされません。その結果、JVM TI関数GetThreadGroupChildren、JDWPコマンドThreadGroupReference/Children、JDIメソッドcom.sun.jdi.ThreadGroupReference.threads()が返すスレッドリストは、プラットフォームスレッドのみを含んでいます。

JDK Flight Recorder (JFR)

JFRは、いくつかの新しいイベントにより、仮想スレッドをサポートします。

  • jdk.VirtualThreadStart および jdk.VirtualThreadEnd は、仮想スレッドの開始と終了を示します。これらのイベントはデフォルトで無効化されています。
  • jdk.VirtualThreadPinned は、仮想スレッドが固定化されたまま、つまりそのプラットフォームスレッドを解放せずに一時停止したことを示します(説明を参照してください)。このイベントはデフォルトで有効になっており、閾値は 20 ミリ秒です。
  • jdk.VirtualThreadSubmitFailed は、仮想スレッドの起動または一時停止に失敗したことを示し、リソースの問題が原因である可能性があります。このイベントはデフォルトで有効になっています。

Java Management Extensions (JMX)

java.lang.management.ThreadMXBean は、プラットフォーム・スレッドの監視および管理のみをサポートします。

findDeadlockedThreads()メソッドは、デッドロック状態にあるプラットフォーム・スレッドのサイクルを検出しますが、デッドロック状態にある仮想スレッドのサイクルは検出しません。

com.sun.management.HotSpotDiagnosticsMXBean の新しいメソッドは、上記の新スタイルのスレッドダンプを生成します。このメソッドは、ローカルまたはリモートの JMX ツールから、プラットフォーム MBeanServer を介して間接的に呼び出せます。

java.lang.ThreadGroup

java.lang.ThreadGroup はスレッドをグループ化するためのレガシー API ですが、最近のアプリケーションではほとんど使われておらず、仮想スレッドをグループ化するのにも適していません。現在では非推奨とし、その地位を低下させ、将来的には構造化された並行処理の一部として新しいスレッド編成構造を導入することを想定しています。

背景として、ThreadGroup APIはJava 1.0から存在しています。これはもともと、グループ内のすべてのスレッドを停止するようなジョブ制御操作を提供することを目的としていました。現代のコードでは、Java 5 で導入された java.util.concurrent パッケージのスレッドプール API を使用することが多くなっています。ThreadGroupは、初期のJavaリリースではアプレットの分離をサポートしていましたが、Javaセキュリティ・アーキテクチャはJava 1.2で大きく進化し、スレッドグループはもはや重要な役割を果たしません。ThreadGroupは、診断目的にも役立つことを意図していましたが、その役割は、java.lang.management APIを含むJava 5で導入された監視・管理機能によって取って代わられました。

ThreadGroupのAPIと実装は、現在ではほとんど無関係であることに加え、いくつかの重大な問題を抱えています。

  • スレッドグループを破棄するための API とメカニズムに不備があります。
  • API は、グループ内のすべての生きているスレッドへの参照を持つことを実装に要求します。これは、スレッド生成、スレッド開始、スレッド終了に同期と競合のオーバーヘッドを追加します。
  • APIは本質的に厳しいenumerate()メソッドを定義しています。
  • API は suspend(), resume(), stop() メソッドを定義していますが、これらは本質的にデッドロックを起こしやすく、安全ではありません。

ThreadGroupは、以下のように指定され、非推奨となり、その地位を低下させられました。

  • スレッドグループを明示的に破棄する機能が削除されました。最終的に非推奨となる destroy() メソッドは何もしません。
  • デーモンスレッドグループの概念が削除されました。最終的に非推奨となる setDaemon(boolean) および isDaemon() メソッドによって設定および取得されるデーモンの状態は無視されます。
  • 実装は、もはやサブグループへの強い参照を保持しません。スレッドグループは、グループ内に生きているスレッドがなく、他にスレッドグループを生かしているものがない場合、ガベージコレクトされる対象になります。
  • 最終的に非推奨となる suspend()、resume()、stop() メソッドは、常に例外を投げます。

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