CodeGym /コース /JAVA 25 SELF /仮想スレッドの作成: Thread.ofVirtual().start()

仮想スレッドの作成: Thread.ofVirtual().start()

JAVA 25 SELF
レベル 57 , レッスン 1
使用可能

1. 実践: 仮想スレッドをどう作るか

そろそろ理論から実践へ進みましょう。従来のやり方でスレッドを作る方法は既にご存じですね。

Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();

もう少し短く書くと:

new Thread(() -> System.out.println("Hi!")).start();

Java 21 からは新しい方法が追加されました:

Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));

あるいは、より明示的に:

Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));

何が違うのか?

  • Thread.ofVirtual().start(...)仮想スレッド(Virtual Thread)を作成します。これは OS ではなく JVM によって管理されます。
  • Thread.ofPlatform().start(...)(または new Thread(...))は、従来のプラットフォームスレッドです。

なぜ重要か

仮想スレッドは OutOfMemoryError を心配せずに数万単位で作成できます。たとえ 100 万件のリクエストを捌こうとしても、Java は「問題ない、どんどん来い!」と言ってくれるでしょう。

2. 仮想スレッド作成の構文

基本例:

public class VirtualThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.ofVirtual().start(() -> {
            System.out.println("仮想スレッドからこんにちは! スレッド: " + Thread.currentThread());
        });
        // スレッドの終了を待つ(main が先に終了しないように)
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

何が起きているか

  • Thread.ofVirtual().start(...) で仮想スレッドを作成しています。
  • スレッドの中ではメッセージを出力するだけの簡単な処理です。
  • 最後に thread.join() を呼び、メインスレッドが仮想スレッドの終了を待つようにします(そうしないと、出力が終わる前にプログラムが終了してしまうことがあります)。

注意:
仮想スレッドは見た目も挙動もほぼ通常のスレッドですが、その内部では JVM のマジックが働いています。

3. 大量生成: Loom の力を実践で

では、従来のスレッドでは危険(あるいは不可能)だったことを試してみましょう。各自が自分の番号を出力する 10_000 個の仮想スレッドを作成します。

public class VirtualThreadMassive {
    public static void main(String[] args) throws InterruptedException {
        int N = 10_000;
        Thread[] threads = new Thread[N];

        for (int i = 0; i < N; i++) {
            int threadNum = i;
            threads[i] = Thread.ofVirtual().start(() -> {
                System.out.println("仮想スレッド #" + threadNum + " が動作中!");
            });
        }

        // すべてのスレッドの終了を待つ
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("すべての仮想スレッドが完了しました!");
    }
}
  • 通常のスレッド(new Thread(...))で同じことをすると、高確率で OutOfMemoryError で落ちます。
  • 仮想スレッドならこれが通常運用です。JVM は数千〜数万のスレッドを難なくさばきます。

ちなみに、10_000 が多いと感じるなら、100_000 やさらには 1_000_000 に挑戦しても構いません。処理が簡単、または I/O 待ち中心であれば、現代のマシンなら JVM は十分対応できます。

4. Runnable とラムダ式: 仮想スレッドにコードを渡す方法

仮想スレッドは通常のスレッドと同様に、インターフェイス Runnable でタスクを受け取ります。つまり、ラムダ式、メソッド参照、そして Runnable を実装した任意のオブジェクトを渡せます。

ラムダの例:

Thread.ofVirtual().start(() -> System.out.println("仮想スレッドでラムダ!"));

メソッド参照の例:

public class TaskRunner {
    public static void main(String[] args) {
        Thread.ofVirtual().start(TaskRunner::doWork);
    }

    static void doWork() {
        System.out.println("仮想スレッドで作業中: " + Thread.currentThread());
    }
}

無名クラスの例:

Thread.ofVirtual().start(new Runnable() {
    @Override
    public void run() {
        System.out.println("仮想スレッドの無名クラス!");
    }
});

まとめ:
通常のスレッドで動いていたものは、仮想スレッドでもそのまま動きます。違うのは「軽くて速い」ことです。

5. ExecutorService との比較: 古いやり方と新しいやり方

従来の ExecutorService

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("タスク #" + taskNum + " を実行中");
    });
}
executor.shutdown();

問題点:
タスクが多くスレッドが少なければ、タスクはキューで待たされます。逆にスレッドを増やしすぎると、リソース不足でプログラムが苦しくなります。

新しいやり方: 仮想スレッドベースの Executor

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("仮想タスク #" + taskNum);
    });
}
executor.shutdown();

何が起きるか?

  • 各タスクごとに専用の仮想スレッドが作られます.
  • JVM がスケジューリングを担い、システムに過負荷をかけません。
  • プールサイズを厳密に制限する必要はありません — 仮想スレッドは「ほぼ無料」です。

仮想スレッドの Executor を使うべきとき

  • 大量のタスクがあり、プールサイズを気にせず投げたいとき。
  • タスク同士が独立しており並列実行できるとき。
  • ExecutorService を使う既存のアーキテクチャ(例: Web サーバ、タスク実行基盤など)に統合したいとき。

アドバイス:
迷ったら、まずは仮想スレッドの Executor から始めましょう。最も汎用的で現代的なやり方です。

6. 実践的な指針: どちらをいつ使うか

Thread.ofVirtual().start() を直接使うべきとき

  • 単発のユニークなタスク(テスト、デモ、簡単な実験など)のために個別スレッドを作りたいとき。
  • スレッド数が少なく、自分で手動管理したいとき。

Executors.newVirtualThreadPerTaskExecutor() を使うべきとき

  • 大量のタスクを起動したいとき(多数のリクエスト、ファイル、ネットワーク接続の処理など)。
  • タスクが独立しており相互調整を必要としないとき。
  • 既存のアーキテクチャで ExecutorService を既に使っている場合(例: Web サーバ、タスク処理系など)。

アドバイス:
もし判断に迷うなら、仮想スレッドの Executor から始めるのが最も無難で汎用的です。

7. 仮想スレッドでの例外処理

仮想スレッドは、try-catch の観点では通常のスレッドと同じです。Runnable の内部で例外が発生しても、JVM 全体が「壊れる」ことはなく、そのスレッドだけがエラーで終了します。

例:

Thread t = Thread.ofVirtual().start(() -> {
    throw new RuntimeException("何かがうまくいきませんでした!");
});
try {
    t.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("メインスレッドは処理を続行しました。");

ExecutorService の場合:
submit でタスクを送る場合、結果は Future で取得でき、get() 呼び出し時に例外がスローされます。

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
    throw new RuntimeException("仮想タスクのエラー");
});
try {
    f.get();
} catch (ExecutionException e) {
    System.out.println("仮想スレッドからのエラーを捕捉: " + e.getCause());
}
executor.shutdown();

8. 仮想スレッド作成時のよくある誤り

エラー No.1: 仮想スレッドとプラットフォームスレッドを混同する。 new Thread(...)Thread.ofPlatform() で作ったものは 仮想スレッドではありません。仮想スレッドになるのは Thread.ofVirtual().start(...)Executors の対応メソッドだけです。

エラー No.2: 重い計算の高速化を期待する。 仮想スレッドは CPU バウンドな処理を速くしません。もし 100 万のスレッドそれぞれで円周率を何百万桁まで計算するなら、JVM は「加速」できず、単にスレッド切り替えが増えるだけです。

エラー No.3: 各スレッドにリソース(例: データベース)を持たせる。 100 万の仮想スレッドを作っても、それぞれが別個のデータベース接続を必要とするなら、データベースが耐えられません。仮想スレッドは、主な時間が待機(I/O)で占められるタスクに向いています。

エラー No.4: 重要なのにスレッドの終了を待たない。 メインスレッドが仮想スレッドより先に終了してしまうと、結果が出る前にプログラムが終わる可能性があります。join() や、ExecutorServiceshutdown()awaitTermination() を使いましょう。

エラー No.5: 仮想スレッドと互換性のない古いライブラリを使う。 一部のサードパーティライブラリは OS レベルでスレッドをブロックしたり、ネイティブ同期を使うため、仮想スレッドの効率を下げます。必ず互換性を確認しましょう。

1
タスク
JAVA 25 SELF, レベル 57, レッスン 1
ロック未解除
レーシングカーのテレメトリ 🏎️
レーシングカーのテレメトリ 🏎️
1
タスク
JAVA 25 SELF, レベル 57, レッスン 1
ロック未解除
実験的アシスタントロボットの管理 🤖
実験的アシスタントロボットの管理 🤖
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION