JEP 425 "Virtual Threads (Preview)"が、JDK 19対象のProposed to TargetからTargetedに昇格した。Project Loomの傘下にあるこのJEPでは、仮想スレッドを導入する。これによって実現される軽量スレッドは、スループットの高いJavaプラットフォーム並列アプリケーションの記述、維持、監視に必要な労力を大幅に軽減するもので、現時点ではプレビュー機能である。
Javaは、メインストリームのプログラミング言語では初めて、並列プログラミング単位としてスレッドを言語のコア機能に取り入れた。従来のJavaスレッド(java.lang.Thread
のインスタンス)は、オペレーティングシステム(OS)スレッド(いわゆるプラットフォームスレッド)の1対1のラッパである。一方の仮想スレッドは、OSではなく、JDKが提供する、Thread
クラスの軽量な実装である。コードの実行にはプラットフォームスレッドを使用するが、コードのライフタイム全体にわたってOSスレッドを捕捉することはなく、複数の仮想スレッドが同一スレッド上でそれぞれのJavaコードを実行できる。
仮想スレッドには数多くのメリットがあるが、中でも重要なのが、スレッドが安価になることと、アプリケーションのスループット向上に寄与することだ。今日のサーバアプリケーションは、ユーザ要求を並列処理するために、それぞれの処理を独立した実行ユニットであるプラットフォームスレッドに委譲している。このような"要求毎のスレッド"プログラミングスタイルは、概念の理解が容易であり、簡単にプログラムできる上、デバッグやプロファイルも難しくはない。その一方で、JDKがスレッドをOSスレッドのラッパとして実装しているため、プラットフォームスレッドの数には制限がある。そのため、アプリケーションのスループット向上にスケールアップが必要である場合でも、プラットフォームスレッドの数的制限のために実現できない場合もある。これに対して、多数の仮想スレッドを生成するのは容易であるため、そのような要求毎スレッドプログラミングスタイルのスケーラビリティに関するボトルネックは緩和される。
また、仮想スレッド上で実行するコードがブロッキングI/Oを呼び出すと、その仮想スレッドは再開が可能になるまで自動的にサスペンドされる。その間、他の仮想スレッドがOSスレッドをテイクオーバして、自身のコードの実行を継続するのだ。同じような状況ではOSスレッドもブロックされるのだが、スレッド数の制限からこれは望ましくない。
仮想スレッドはスレッドローカル変数や同期ブロック、スレッド割込みといったものをサポートするが、Java開発者にとってこれは、仮想スレッドは単に安価かつリソースの豊富なスレッドであって、プログラミングのアプローチはまったく変化しないということになる。旧来のスレッドを対象に記述された既存のJavaコードは、変更を必要とせず、仮想スレッド上で簡単に実行することができる。
仮想スレッドをぜひ試してみたいという開発者は、JDK 19の早期アクセスビルドがダウンロードして、それに慣れておくとよいだろう。Thread
クラスには、新たなメソッドとしてstartVirtualThread(Runnable)
が提供されている。このメソッドはRunnable
を引数として、仮想スレッドを生成する。以下の例を考えてみよう。
public class Main {
public static void main(String[] args) throws InterruptedException {
var vThread = Thread.startVirtualThread(() -> {
System.out.println("Hello from the virtual thread");
});
vThread.join();
}
}
プレビュー機能であるため、このコードをコンパイルするには、次のコマンドのように--enable-preview
フラグを指定する必要がある。
javac --release 19 --enable-preview Main.java
プログラム実行時にも同じフラグが必要だ。
java --enable-preview Main
ソースコードランチャを使って直接実行することもできる。その場合のコマンドラインは次のようなものになる。
java --source 19 --enable-preview Main.java
jshellも使用可能だが、同じようにプレビュー機能を有効にする必要がある。
jshell --enable-preview
Thread.startVirtualThread(Runnable)
を使えば、仮想スレッドを簡単に生成することができる。また、仮想スレッドとプラットフォームスレッドを生成するために、Thread.Builder
、Thread.ofVirtual()
、Thread.ofPlatform()
などのAPIが新たに設けられた。
公開コンストラクタを使って仮想スレッドを生成することはできない。また、仮想スレッドはNORM_PRIORITY
のデーモンスレッドとして動作する。Thread.setPriority(int)
やThread.setDaemon(boolean)
を使って仮想スレッドの優先度を変更したり、非デーモンスレッドにすることはできない。また、仮想スレッドはアクティブなスレッドによって実行されるので、ThreadGroup
に含めることも不可能だ。Thread.getThreadGroup()
の呼び出しに対しては、"VirtualThreads"が返される。
仮想スレッドは、存在期間中に単一のタスクを実行することを意図している。従って、スレッドのプールは適切でなく、制約のない生成をプログラミングモデルとする必要がある。この目的のために、無制限のエグゼキュータ(executor)が追加されており、新設されたファクトリメソッドExecutors.newVirtualThreadPerTaskExecutor()
を通じてアクセスできる。これらのメソッドにより、スレッドプールとExecutorService
を使用する既存コードのマイグレーションや互換性が実現されている。以下の例を考えてみよう。
import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
}
}
仮想スレッドの既定のスケジューラは、ForkJoinPool
で導入されたワークスティーリング(work-stealing)スケジューラである。
仮想スレッドの詳細やバックストーリに興味があるならば、Oracleテクニカルスタッフのコンサルティングメンバで、Project Loomのプロジェクトリーダを務めるRon Pressler氏による、InfoQ PodcastやYouTubeのセッションを聞いてみるとよいだろう。