仮想スレッドサポートリファレンス
このガイドでは、QuarkusアプリケーションでJava 21+の仮想スレッドを使用する方法について説明します。
仮想スレッドとは?
用語解説
- OSスレッド
-
オペレーティング・システムによって管理される「スレッドのような」データ構造。
- プラットフォームスレッド
-
Java 19までは、 Thread クラスのインスタンスはすべて、OSスレッドのラッパーであるプラットフォーム・スレッドでした。 プラットフォーム・スレッドを作成するとOSスレッドが作成され、プラットフォーム・スレッドをブロックするとOSスレッドがブロックされます。
- 仮想スレッド
-
軽量で、JVMが管理するスレッド。 Thread クラスを拡張していますが、1つの特定のOSスレッドに縛られることはありません。したがって、仮想スレッドのスケジューリングはJVMの責任です。
- キャリアスレッド
-
仮想スレッドを実行するために使用されるプラットフォームスレッドは、 キャリアスレッド と呼ばれます。 これは Thread や
VirtualThread
とは異なるクラスではなく、機能的な呼称です。
仮想スレッドとプラットフォームスレッドの違い
ここではその概要を説明します。詳細は JEP425 を参照してください。
仮想スレッドは、Java 19から利用可能な機能で(Java 21は、仮想スレッドを含む最初のLTSバージョン)、 I/Oバウンドのワークロードに対して、プラットフォームスレッドの安価な代替を提供することを目的としています。
これまで、プラットフォーム・スレッドがJVMの同時実行ユニットでした。 スレッドは、OS構造体のラッパーです。 Javaプラットフォーム・スレッドを作成すると、オペレーティング・システムに「スレッドのような」構造が作成されます。
一方、仮想スレッドはJVMによって管理されます。実行するには、(その仮想スレッドのキャリアとして機能する)プラットフォーム・スレッドにマウントする必要があります。 そのため、以下のような特徴があります:
- 軽量
-
仮想スレッドは、プラットフォーム・スレッドよりもメモリ上の占有領域が小さくなります。 したがって、メモリを消費することなく、プラットフォーム・スレッドよりも多くの仮想スレッドを同時に使用することが可能になります。 デフォルトでは、プラットフォーム・スレッドは約1MBのスタックで作成されますが、仮想スレッドのスタックは "pay-as-you-go "です。 Loomプロジェクト(JVMに仮想スレッドサポートを追加したプロジェクト)のリード開発者が行った プレゼンテーション で、これらの数字や仮想スレッドの他の動機を見つけることができます。
- 作成が安価
-
Javaでプラットフォーム・スレッドを作成するには時間がかかります。 現在では、スレッドを一度作成してから再利用するプーリングのようなテクニックが、スレッドの起動にかかる時間のロスを最小限に抑えるために強く推奨されています(同様に、メモリ消費量を低く抑えるためにスレッドの最大数を制限することも推奨されています)。 仮想スレッドは必要なときに作成する使い捨てのものであり、 スレッドをプールしたり別のタスクに再利用したりすることは推奨されません。
- 安価なブロック
-
ブロッキングI/Oを実行するとき、Javaプラットフォームのスレッドによってラップされた基礎となるOSスレッドは待ち行列に入れられ、新しいスレッドコンテキストをCPUコアにロードするためにコンテキストスイッチが発生します。 この操作には時間がかかります。JVMは仮想スレッドを管理するため、OSスレッドがブロッキング操作を実行しても、そのスレッドがブロックされることはありません。 それらの状態はヒープに格納され、別の仮想スレッドが同じJavaプラットフォーム(キャリア)スレッド上で実行されます。
継続ダンス
As mentioned above, the JVM schedules the virtual threads. These virtual threads are mounted on carrier threads. The scheduling comes with a pinch of magic. When the virtual thread attempts to use blocking I/O, the JVM transforms this call into a non-blocking one, unmounts the virtual thread, and mounts another virtual thread on the carrier thread. When the I/O completes, the waiting virtual thread becomes eligible again and will be re-mounted on a carrier thread to continue its execution. For the user, all this dance is invisible. Your synchronous code is executed asynchronously.
仮想スレッドを同じキャリアスレッドに再マウントすることはできません。
仮想スレッドはI/Oバウンドワークロードにのみに有効
We now know we can create more virtual threads than platform threads. One could be tempted to use virtual threads to perform long computations (CPU-bound workload). It is useless and counterproductive. CPU-bound doesn’t consist of quickly swapping threads while they need to wait for the completion of an I/O, but in leaving them attached to a CPU core to compute something. In this scenario, it is worse than useless to have thousands of threads if we have tens of CPU cores, virtual threads won’t enhance the performance of CPU-bound workloads. Even worse, when running a CPU-bound workload on a virtual thread, the virtual thread monopolizes the carrier thread on which it is mounted. It will either reduce the chance for the other virtual thread to run or will start creating new carrier threads, leading to high memory usage.
@RunOnVirtualThread を使用した仮想スレッド上でのコード実行
In Quarkus, the support of virtual thread is implemented using the @RunOnVirtualThread annotation. This section briefly overviews the rationale and how to use it. There are dedicated guides for extensions supporting that annotation, such as:
なぜすべてを仮想スレッドで実行しないのか?
前述したように、すべてが仮想スレッド上で安全に実行できるわけではありません。
独占 のリスクはメモリ使用量の多さにつながります。
また、仮想スレッドをキャリアスレッドからアンマウントできない状況もあります。
これは pinning と呼ばれます。
最後に、一部のライブラリは ThreadLocal
を使用してオブジェクトを保存し再利用します。
これらのライブラリで仮想スレッドを使用すると、意図的にプールされたオブジェクトが(使い捨てで一般に短命な)仮想スレッドごとにインスタンス化されるため、大量の割り当てが発生します。
現在のところ、仮想スレッドを自由に使うことはできません。 このような自由放任のアプローチをとると、すぐにメモリやリソースの枯渇という問題が発生します。 そのためQuarkusでは、前述の問題がなくなるまで(Javaエコシステムが成熟するまで)、明示的なモデルを使用します。 リアクティブ エクステンションが仮想スレッドをサポートし、 古典的な スレッドをほとんどサポートしない理由もここにあります。 仮想スレッドでディスパッチするタイミングを知る必要があります。
これらの問題は、Quarkusの制限やバグではなく、仮想スレッドフレンドリーに進化する必要があるJavaエコシステムの現状によるものであることを理解することが重要です。
内部設計と選択の詳細については、 Considerations for integrating virtual threads in Java framework: a Quarkus example in resource-constrained environment を参照してください。 |
Monopolization cases
独占については、 仮想スレッドはI/Oバウンドのワークロードにのみ有効 のセクションで説明しました。 長い計算を実行する場合、仮想スレッドが終了するまで、JVMがアンマウントして別の仮想スレッドに切り替えることを許可しません。 実際、現在のスケジューラはタスクのプリエンプトをサポートしていません。
この独占は、他の仮想スレッドを実行するために新しいキャリアスレッドを作成することにつながります。 キャリア・スレッドを作成すると、プラットフォーム・スレッドを作成することになります。 そのため、この作成にはメモリ・コストがかかります。
コンテナのような制約のある環境で実行するとします。 その場合、メモリ使用量の多さがメモリ不足の問題やコンテナの終了につながる可能性があるため、独占はすぐに懸念事項となります。 メモリ使用量は、スケジューリングと仮想スレッドに固有のコストがかかるため、通常のワーカースレッドよりも高くなる可能性があります。
拘束の場合
The promise of "cheap blocking" might not always hold: a virtual thread might pin its carrier on certain occasions. The platform thread is blocked in this situation, precisely as it would have been in a typical blocking scenario.
JEP 425 によると、これは2つの状況で発生する場合があります:
-
仮想スレッドが
synchronized
ブロックまたはメソッドの内部でブロッキング操作を実行する場合 -
ネイティブメソッドや外部関数内でブロッキング操作を実行した場合
It can be reasonably easy to avoid these situations in your code, but verifying every dependency you use is hard. Typically, while experimenting with virtual threads, we realized that versions older than 42.6.0 of the postgresql-JDBC driver result in frequent pinning. Most JDBC drivers still pin the carrier thread. Even worse, many libraries require code changes.
For more information, see When Quarkus meets Virtual Threads
This information about pinning cases applies to PostgreSQL JDBC driver 42.5.4 and earlier. For PostgreSQL JDBC driver 42.6.0 and later, virtually all synchronized methods have been replaced by reentrant locks. For more information, see the Notable Changes for PostgreSQL JDBC driver 42.6.0. |
The pooling case
Some libraries are using ThreadLocal
as an object pooling mechanism.
Extremely popular libraries like Jackson and Netty assume that the application uses a limited number of threads, which are recycled (using a thread pool) to run multiple (unrelated but sequential) tasks.
This pattern has multiple advantages, such as:
-
Allocation benefit: heavy objects are only allocated once per thread, but because the number of these threads was intended to be limited, it would not use too much memory.
-
Thread safety: only one thread can access the object stored in the thread local - preventing concurrent accesses.
However, this pattern is counter-productive when using virtual threads.
Virtual threads are not pooled and generally short-lived.
So, instead of a few of them, we now have many of them.
For each of them, the object stored in the ThreadLocal
is created (often large and expensive) and won’t be reused, as the virtual thread is not pooled (and won’t be used to run another task once the execution completes).
This problem leads to high memory usage.
Unfortunately, it requires sophisticated code changes in the libraries themselves.
Use @RunOnVirtualThread with Quarkus REST (formerly RESTEasy Reactive)
このセクションでは、 @RunOnVirtualThread アノテーションの簡単な使用例を示します。 また、Quarkusが提供するさまざまな開発モデルと実行モデルについても説明します。
@RunOnVirtualThread
アノテーションは、Quarkusに対して、現在のスレッドではなく 新しい 仮想スレッドでアノテーションされたメソッドを呼び出すように指示します。
Quarkusは仮想スレッドの作成とオフロードを処理します。
Since virtual threads are disposable entities, the fundamental idea of @RunOnVirtualThread
is to offload the execution of an endpoint handler on a new virtual thread instead of running it on an event-loop or worker thread (in the case of Quarkus REST).
そのためには、エンドポイントに @RunOnVirtualThread アノテーションを追加すれば十分です。 アプリケーションの 実行 に使用される Java 仮想マシンが仮想スレッドのサポートを提供している場合 (Java 21 以降のバージョン)、エンドポイントの実行は仮想スレッドにオフロードされます。 これにより、仮想スレッドがマウントされているプラットフォーム・スレッドをブロックすることなく、ブロッキング処理を実行できるようになります。
In the case of Quarkus REST, this annotation can only be used on endpoints annotated with @Blocking or considered blocking because of their signature. You can visit Execution model, blocking, non-blocking for more information.
Get started with virtual threads with Quarkus REST
ビルドファイルに以下の依存関係を追加します:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest")
Then, you also need to make sure that you are using Java 21+, this can be enforced in your pom.xml file with the following:
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
3つの開発・実行モデル
以下の例は、3つのエンドポイントの違いを示しています。これらのエンドポイントはすべて、データベース上の 財産 を照会し、それをクライアントに返します。
-
1つ目は伝統的なブロッキングスタイルを採用しており、そのシグネチャからブロッキングとみなされます。
-
2つ目はMutinyを使用し、そのシグネチャによりノンブロッキングとみなされます。
-
3番目はMutinyを使用しますが、同期的な方法で使用し、"リアクティブ型"を返さないので、ブロッキングとみなされ、 @RunOnVirtualThread アノテーションが使用できます。
package org.acme.rest;
import org.acme.fortune.model.Fortune;
import org.acme.fortune.repository.FortuneRepository;
import io.smallrye.common.annotation.RunOnVirtualThread;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.List;
import java.util.Random;
@Path("")
public class FortuneResource {
@Inject FortuneRepository repository;
@GET
@Path("/blocking")
public Fortune blocking() {
// Runs on a worker (platform) thread
var list = repository.findAllBlocking();
return pickOne(list);
}
@GET
@Path("/reactive")
public Uni<Fortune> reactive() {
// Runs on the event loop
return repository.findAllAsync()
.map(this::pickOne);
}
@GET
@Path("/virtual")
@RunOnVirtualThread
public Fortune virtualThread() {
// Runs on a virtual thread
var list = repository.findAllAsyncAndAwait();
return pickOne(list);
}
}
以下の表は各選択肢の概要です:
モデル | シグネチャの例 | 長所 | 短所 |
---|---|---|---|
同期コードでワーカースレッド上 |
|
シンプルなコード |
ワーカースレッドを使用 (同時実行に制限) |
リアクティブコードでイベントループ上 |
|
高い同時実行性と低いリソース使用量 |
より複雑なコード |
同期コードで仮想スレッド上 |
|
シンプルなコード |
ピン止め、独占、効率の悪いオブジェクト・プーリングのリスク |
この3つのモデルすべてを1つのアプリケーションで使用できます。
仮想スレッドフレンドリーなクライアントの使用
As mentioned in the Why not run everything on virtual threads? section, the Java ecosystem is not entirely ready for virtual threads. So, you need to be careful, especially when using a libraries doing I/O.
Fortunately, Quarkus provides a massive ecosystem that is ready to be used in virtual threads. Mutiny, the reactive programming library used in Quarkus, and the Vert.x Mutiny bindings provides the ability to write blocking code (so, no fear, no learning curve) which do not pin the carrier thread.
As a result:
-
Quarkus extensions providing blocking APIs on top of reactive APIs can be used in virtual threads. This includes the REST Client, the Redis client, the mailer…
-
API returning
Uni
can be used directly usinguni.await().atMost(…)
. It blocks the virtual thread, without blocking the carrier thread, and also improves the resilience of your application with an easy (non-blocking) timeout support. -
If you use a Vert.x client using the Mutiny bindings, use the
andAwait()
methods which block until you get the result without pinning the carrier thread. It includes all the reactive SQL drivers.
テストでピン止めされたスレッドを検出
仮想スレッドを使用するアプリケーションでテストを実行する場合は、以下の設定を使用することをお勧めします。 テストが失敗することはありませんが、コードがキャリアのスレッドをピン留めした場合は、少なくともスタートトレースをダンプします:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<argLine>-Djdk.tracePinnedThreads</argLine>
</configuration>
</plugin>
仮想スレッドを使用したアプリケーションの実行
java -jar target/quarkus-app/quarkus-run.jar
Java 21以前では、仮想スレッドはまだ実験的な機能であったため、 --enable-preview フラグを付けてアプリケーションを起動する必要があります。
|
仮想スレッドを使用したアプリケーションのコンテナのビルド
アプリケーションをJVMモードで実行する場合(つまりネイティブにコンパイルしない場合、ネイティブについては 専用のセクション を参照してください)、 コンテナ化ガイド に従ってコンテナをビルドすることができます。
このセクションでは、JIB を使用してコンテナをビルドします。 他の方法については、 コンテナ化ガイド を参照してください。
@RunOnVirtualThread
を使用する Quarkus アプリケーションをコンテナ化するには、 application.properties
に以下のプロパティを追加します:
quarkus.container-image.build=true
quarkus.container-image.group=<your-group-name>
quarkus.container-image.name=<you-container-name>
quarkus.jib.base-jvm-image=registry.access.redhat.com/ubi8/openjdk-21-runtime (1)
quarkus.jib.platforms=linux/amd64,linux/arm64 (2)
1 | Make sure you use a base image supporting virtual threads. Here we use an image providing Java 21. Quarkus picks an image providing Java 21+ automatically if you do not set one. |
2 | Select the target architecture. You can select more than one to build multi-archs images. |
次に、通常と同じようにコンテナをビルドします。 例えば、Mavenを使用している場合、次を実行します:
mvn package
仮想スレッドを使用したQuarkusアプリケーションのネイティブ実行可能ファイルへのコンパイル
ローカルにインストールしたGraalVMの使用
To compile a Quarkus applications leveraging @RunOnVirtualThread
into a native executable, you must be sure to use a GraalVM / Mandrel native-image
supporting virtual threads, so providing at least Java 21.
Build the native executable as indicated on the native compilation guide. For example, with Maven, run:
mvn package -Dnative
コンテナ内ビルドの使用
コンテナ内ビルドでは、コンテナ内で動作する native-image
コンパイラを使用して Linux 64 実行可能ファイルをビルドできます。
これは、 native-image
をマシンにインストールする手間を省き、必要な GraalVM バージョンを設定することもできます。
コンテナ内ビルドを使用するには、DockerまたはPodmanがマシンにインストールされている必要があることに注意してください。
そして、 application.properties
ファイルに追加します:
# In-container build to get a linux 64 executable
quarkus.native.container-build=true (1)
1 | コンテナ内ビルドを有効にします。 |
ARM/64からAMD/64へ
Mac M1またはM2(ARM64 CPUを使用)を使用している場合、コンテナ内ビルドを使用して取得するネイティブ実行可能ファイルはLinux実行可能ファイルですが、ホスト(ARM64)アーキテクチャを使用していることに注意する必要があります。 以下のプロパティでDockerを使用する際に、エミュレーションを使用して強制的にアーキテクチャを変更することができます:
コンパイル時間がかなり長くなる(10分以上)ことに注意してください。 |
仮想スレッドを使用したネイティブ・アプリケーションのコンテナ化
ネイティブ実行ファイルにコンパイルされた仮想スレッドを使用してQuarkusアプリケーションを実行するコンテナをビルドするには、 Linux/AMD64実行ファイル(ARMマシンを対象としている場合はARM64)を用意する必要があります。
application.properties
に ネイティブ・コンパイルのセクション で説明した設定が含まれているようにしてください。
次に、通常と同じようにコンテナをビルドします。 例えば、Mavenを使用している場合、次を実行します:
mvn package -Dnative
ネイティブコンテナイメージをビルドしたいときに、すでに既存のネイティブイメージがある場合は、 -Dquarkus.native.reuse-existing=true を設定すれば、ネイティブイメージのビルドは再実行されません。
|
仮想スレッドで複製されたコンテキストの使用
@RunOnVirtualThread
でアノテーションされたメソッドは、複製された元のコンテキストを継承します (詳細は 複製されたコンテキストのリファレンスガイド を参照ください)。
そのため、フィルタやインターセプタによって複製されたコンテキストに書き込まれたデータ (およびリクエストスコープが複製されたコンテキストに格納されているため、リクエストスコープ) は、メソッドの実行中に (フィルタやインターセプタが仮想スレッド上で実行されていなくても) 利用可能です。
ただし、スレッドローカルは伝播されません。
仮想スレッド名
仮想スレッドは、デフォルトではスレッド名なしで作成されます。これは、デバッグやロギング目的で実行を識別するためには実用的ではありません。
Quarkusの管理対象仮想スレッドには名前が付けられ、 quarkus-virtual-thread-
というプレフィックスが付けられます。
このプレフィックスはカスタマイズすることも、空の値を設定して命名を完全に無効にすることもできます:
quarkus.virtual-threads.name-prefix=
仮想スレッドエグゼキュータの注入
仮想スレッド上でタスクを実行するために、Quarkusは内部 ThreadPerTaskExecutor
を管理します。
まれにこのエクゼキューターに直接アクセスする必要がある場合は、 @VirtualThreads
CDI修飾子を使用して注入できます:
Virtual Thread ExecutorService の注入は実験的なもので、将来のバージョンで変更される可能性があります。 |
package org.acme;
import org.acme.fortune.repository.FortuneRepository;
import java.util.concurrent.ExecutorService;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import io.quarkus.logging.Log;
import io.quarkus.runtime.StartupEvent;
import io.quarkus.virtual.threads.VirtualThreads;
public class MyApplication {
@Inject
FortuneRepository repository;
@Inject
@VirtualThreads
ExecutorService vThreads;
void onEvent(@Observes StartupEvent event) {
vThreads.execute(this::findAll);
}
@Transactional
void findAll() {
Log.info(repository.findAllBlocking());
}
}
仮想スレッドアプリケーションのテスト
上述したように、仮想スレッドにはいくつかの制限があり、 アプリケーションのパフォーマンスやメモリ使用量に大きな影響を与える可能性があります。 junit5-virtual-threads エクステンションは、テストの実行中にピン留めされたキャリアスレッドを検出する方法を提供します。 このため、最も顕著な制限のひとつを取り除いたり、 問題に気づいたりすることができます。
この検出を有効にするには
-
1)
junit5-virtual-threads
の依存関係をプロジェクトに追加します:
<dependency> <groupId>io.quarkus.junit5</groupId> <artifactId>junit5-virtual-threads</artifactId> <scope>test</scope> </dependency>
-
2) テストケースに、
io.quarkus.test.junit5.virtual.VirtualThreadUnit
とio.quarkus.test.junit.virtual.ShouldNotPin
のアノテーションを追加します:
@QuarkusTest @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @VirtualThreadUnit // Use the extension @ShouldNotPin // Detect pinned carrier thread class TodoResourceTest { // ... }
テストを実行すると(Java 21+を使用することを忘れないでください)、Quarkusはピンされたキャリアスレッドを検出します。 この場合、テストは失敗します。
@ShouldNotPin
はメソッドに直接使うこともできます。
junit5-virtual-threadsは 、ピニングが避けられない場合のために @ShouldPin
アノテーションも提供しています。
次のスニペットは @ShouldPin
アノテーションの使い方を示しています。
@VirtualThreadUnit // Use the extension
public class LoomUnitExampleTest {
CodeUnderTest codeUnderTest = new CodeUnderTest();
@Test
@ShouldNotPin
public void testThatShouldNotPin() {
// ...
}
@Test
@ShouldPin(atMost = 1)
public void testThatShouldPinAtMostOnce() {
codeUnderTest.pin();
}
}