1. 序章

このチュートリアルでは、Javaの従来のスレッドと ProjectLoomで導入された仮想スレッドの違いを示します。

次に、プロジェクトで導入された仮想スレッドとAPIのいくつかのユースケースを共有します。

始める前に、注意する必要がありますこのプロジェクトは活発に開発されています。 早期アクセスルームVMで例を実行します:openjdk-15-loom+4-55_windows-x64_bin。

ビルドの新しいバージョンは、現在のAPIを自由に変更および破壊できます。 そうは言っても、以前に使用されていた java.lang.Fiber クラスが削除され、新しい java.lang.VirtualThread クラスに置き換えられたため、APIにはすでに大きな変更がありました。 。

2. スレッドとの概要 仮想スレッド

大まかに言えば、スレッドはオペレーティングシステムによって管理およびスケジュールされ、仮想スレッドは仮想マシンによって管理およびスケジュールされます。 ここで、新しいカーネルスレッドを作成するには、システムコールを実行する必要がありますが、これはコストのかかる操作です

そのため、必要に応じてスレッドの再割り当てと割り当て解除を行う代わりに、スレッドプールを使用しています。 次に、コンテキストスイッチングとそのメモリフットプリントのために、スレッドを追加してアプリケーションをスケーリングする場合、それらのスレッドを維持するためのコストが高くなり、処理時間に影響を与える可能性があります。

次に、通常、これらのスレッドをブロックしたくないため、非ブロックI / O APIと非同期APIが使用され、コードが乱雑になる可能性があります。

それどころか、仮想スレッドはJVMによって管理されます。 したがって、それらの割り当てはシステムコールを必要とせず、オペレーティングシステムのコンテキストスイッチがありません。 さらに、仮想スレッドは、内部で使用される実際のカーネルスレッドであるキャリアスレッドで実行されます。 その結果、システムのコンテキストスイッチがないため、このような仮想スレッドをさらに多く生成できます。

次に、仮想スレッドの重要な特性は、キャリアスレッドをブロックしないことです。 これにより、JVMが別の仮想スレッドをスケジュールし、キャリアスレッドをブロックしないままにするため、仮想スレッドのブロックははるかに安価な操作になります。

最終的には、NIOまたは非同期APIについて問い合わせる必要はありません。 これにより、コードが読みやすくなり、理解とデバッグが容易になります。 それでも、継続によってキャリアスレッドがブロックされる可能性があります。具体的には、スレッドがネイティブメソッドを呼び出し、そこからブロック操作を実行する場合です。

3. 新しいスレッドビルダーAPI

Loomでは、 Thread クラスの新しいビルダーAPIと、いくつかのファクトリメソッドを取得しました。 標準ファクトリと仮想ファクトリを作成し、それらをスレッドの実行に利用する方法を見てみましょう。

Runnable printThread = () -> System.out.println(Thread.currentThread());
        
ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory();
ThreadFactory kernelThreadFactory = Thread.builder().factory();

Thread virtualThread = virtualThreadFactory.newThread(printThread);
Thread kernelThread = kernelThreadFactory.newThread(printThread);

virtualThread.start();
kernelThread.start();

上記の実行の出力は次のとおりです。

Thread[Thread-0,5,main]
VirtualThread[<unnamed>,ForkJoinPool-1-worker-3,CarrierThreads]

ここで、最初のエントリは、カーネルスレッドの標準のtoString出力です。

これで、出力に仮想スレッドに名前がなく、CarrierThreadsスレッドグループのFork-Joinプールのワーカースレッドで実行されていることがわかります。

ご覧のとおり、基盤となる実装に関係なく、 APIは同じであり、仮想スレッドで既存のコードを簡単に実行できることを意味します。

また、それらを利用するために新しいAPIを学ぶ必要はありません。

4. 仮想スレッド構成

これは、継続とスケジューラであり、一緒になって仮想スレッドを構成します。 現在、ユーザーモードスケジューラは、Executorインターフェイスの任意の実装である可能性があります。 上記の例は、デフォルトでForkJoinPoolで実行されることを示しています。

これで、カーネルスレッドと同様に、CPUで実行し、パークして再スケジュールし、実行を再開できます。継続とは、開始してからパーク(降伏)、再スケジュール、再開できる実行ユニットです。オペレーティングシステムに依存するのではなく、中断したところから同じ方法で実行され、JVMによって管理されます。

継続は低レベルのAPIであり、プログラマーはビルダーAPIなどの高レベルのAPIを使用して仮想スレッドを実行する必要があることに注意してください。

ただし、内部でどのように機能するかを示すために、実験の継続を実行します。

var scope = new ContinuationScope("C1");
var c = new Continuation(scope, () -> {
    System.out.println("Start C1");
    Continuation.yield(scope);
    System.out.println("End C1");
});

while (!c.isDone()) {
    System.out.println("Start run()");
    c.run();
    System.out.println("End run()");
}

上記の実行の出力は次のとおりです。

Start run()
Start C1
End run()
Start run()
End C1
End run()

この例では、継続を実行し、ある時点で処理を停止することにしました。 その後、再実行すると、中断したところから続行しました。 出力から、 run()メソッドが2回呼び出されたことがわかりますが、継続は1回開始され、中断したところから2回目の実行で実行を継続しました。

これは、ブロッキング操作がJVMによって処理されることを意味する方法です。 ブロック操作が発生すると、継続が発生し、キャリアスレッドはブロックされないままになります。

つまり、メインスレッドが run()メソッドの呼び出しスタックに新しいスタックフレームを作成し、実行を続行したということです。 次に、継続が生成された後、JVMは実行の現在の状態を保存しました。

次に、メインスレッドは run()メソッドが戻ってきたかのように実行を継続し、whileループを続行します。 継続のrunメソッドへの2回目の呼び出しの後、JVMはメインスレッドの状態を、継続が生成されて実行が終了したポイントに復元しました。

5. 結論

この記事では、カーネルスレッドと仮想スレッドの違いについて説明しました。 次に、ProjectLoomの新しいスレッドビルダーAPIを使用して仮想スレッドを実行する方法を示しました。

最後に、継続とは何か、そしてそれが内部でどのように機能するかを示しました。 アーリーアクセスVMを調べることで、ProjectLoomの状態をさらに詳しく調べることができます。 または、すでに標準化されているJava同時実行性APIをさらに調べることもできます。