3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは、もんすんです。

前回のJava11→17編では switch 式・テキストブロック・recordsealed と、書き方を変える機能が一気に登場しました。

今回は Java17→21編です。

Java21も LTS で、その目玉は仮想スレッド(Virtual Threads)です。

JavaでのWeb開発の多くは Spring Boot を介しているため、Virtual Threads を直接書かなくても恩恵を受けられる場面が多いです。
でもその仕組みを知っておくことで、パフォーマンス問題に対する見方が変わります。

また、前回登場した sealed と今回のパターンマッチングが合わさると、switch の書き方がさらに進化します。
この接続も最後に見ていきましょう。

対象読者

  • Java11→17編を読んだ方(または Java17 の主要機能を知っている方)
  • Virtual Threads に興味があるが、何がどう変わるのか掴めていない方

Java17からJava21へのレベルアップ

1. Virtual Threads(仮想スレッド)(Java 21 で正式へ)

「スレッドが多いと重い」の常識を変える機能です。

まず前提を整理します。

従来のスレッド(プラットフォームスレッド)

Javaでスレッドを1本立てると、OSのスレッドが1本対応します。
OSスレッドはメモリ(スタックで数百KB〜数MB)を消費し、生成・切り替えにコストがかかります。

// 従来のスレッド
var thread = new Thread(() -> {
    System.out.println("スレッドで実行");
});
thread.start();

Web サーバーでは「リクエスト1件 = スレッド1本」というモデルが一般的でした。
しかしスレッドは重いので、高負荷時にスレッドが枯渇する問題がありました。

これを回避するために非同期プログラミング(CompletableFuture や WebFlux / Reactor)が使われてきました。

しかし非同期コードは難しい
コールバック地獄、エラーハンドリングの複雑さ、デバッグのしにくさ...など、慣れていないと一気に詰まります。

Virtual Threads が解決すること:sparkles:

// Virtual Thread の作成
var vThread = Thread.ofVirtual().start(() -> {
    System.out.println("仮想スレッドで実行");
});

// Thread.startVirtualThread() でも同じ
Thread.startVirtualThread(() -> {
    System.out.println("仮想スレッドで実行");
});

Virtual Threads は JVM が管理する軽量スレッドです。

プラットフォームスレッド Virtual Threads
対応するOSスレッド 1:1 多数のVirtualThreadが少数のOSスレッドを共有
メモリ 数百KB〜数MB 数KB(必要に応じて伸縮)
生成コスト 高い 非常に低い
数の上限 数千程度が現実的 数百万も可能

Virtual Threads が I/O 待ちになると、JVM が自動的に別の Virtual Thread に切り替えます。スレッドがブロックされたまま OSスレッドを占有し続けないので、少ない OSスレッドで大量の並行処理をさばくことができます!

書き方は同期コードのまま

// Virtual Threads を使っても、書き方は普通の同期コード
Thread.startVirtualThread(() -> {
    var response = httpClient.send(request, bodyHandler);  // ここでブロックされても OK
    System.out.println(response.body());
});

非同期で書いたような結果を、同期コードの読みやすさで達成できます。

Spring Boot での活用(実務の本題)

Spring Boot 3.2 以降では、Virtual Threads を1行の設定で有効化できます。

# application.yml
spring:
  threads:
    virtual:
      enabled: true

これだけで、Tomcat が Virtual Threads を使うようになります。
従来のスレッドプールサイズの調整が不要になり、高スループットを同期コードで実現できます。

:warning:注意点もある

  • スレッドローカル変数の使い方に注意
    • Virtual Threads は大量に生成される
    • そのため、ThreadLocal大きいオブジェクトを詰め込むとメモリ問題になりうる
  • synchronized ブロックのピン留め問題
    • synchronized ブロック内で Virtual Thread がブロックされると、対応する OSスレッドも固定されてしまい、Virtual Threads の恩恵が減る
    • ReentrantLock を使うと回避できる
  • CPU バウンドな処理には効かない
    • Virtual Threads が輝くのは I/O 待ち(データベースアクセス・HTTP 通信等)の多い処理
    • 逆にCPU を使い続ける処理は従来のスレッドと変わらない

2. パターンマッチング for switch(Java 21 から正式へ)

前回の sealed クラスとここで合流します。instanceof のパターンマッチング(Java16)がそのまま switch 式で使えるようになりました。

Before(Java 17 以前)

static String format(Object obj) {
    if (obj instanceof Integer i) {
        return "整数: " + i;
    } else if (obj instanceof String s) {
        return "文字列: " + s;
    } else if (obj instanceof Double d) {
        return "小数: " + d;
    } else {
        return "その他: " + obj;
    }
}

After(Java 21 以降)

static String format(Object obj) {
    return switch (obj) {
        case Integer i -> "整数: " + i;
        case String s  -> "文字列: " + s;
        case Double d  -> "小数: " + d;
        default        -> "その他: " + obj;
    };
}

型で分岐するコードがスッキリします。

sealed クラスとの組み合わせ

// 前回の sealed クラス
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}

// Java 21 以降のパターンマッチング switch
static double area(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.w() * r.h();
        case Triangle t  -> t.base() * t.height() / 2;
        // sealed で型が限定されているため default 不要 → コンパイラが網羅を確認
    };
}

sealed で継承先が限定されているので、default なしで全ケースを網羅できます!
新しい型を追加したとき、switch のコンパイルエラーで追加漏れに気づけます。安全です。:ribbon:

3. record パターン(Java 21 から正式へ)

record の中身をパターンマッチングで分解できる構文です。

record Point(int x, int y) {}

Object obj = new Point(3, 5);

// record の中身を switch 内で分解
if (obj instanceof Point(int x, int y)) {
    System.out.println("x=" + x + ", y=" + y);  // x=3, y=5
}

// switch 式でも同様
String result = switch (obj) {
    case Point(int x, int y) -> "座標: (" + x + ", " + y + ")";
    default -> "不明";
};

ネストした record でも使えます。

record Line(Point start, Point end) {}

if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
    // x1, y1, x2, y2 がそのまま使える
}

4. Sequenced Collections(Java 21)

「順序を持つコレクション」を統一的に扱うインターフェースが追加されました。

// 先頭・末尾の取得が統一的に
List<String> list = new ArrayList<>(List.of("A", "B", "C"));

list.getFirst();  // "A"
list.getLast();   // "C"

list.addFirst("Z");  // 先頭に追加
list.addLast("D");   // 末尾に追加

list.removeFirst();  // 先頭を削除
list.removeLast();   // 末尾を削除

// reversed() で逆順ビュー
for (var item : list.reversed()) {
    System.out.println(item);
}

LinkedListgetFirst() / getLast() は以前からありましたが、ArrayListDeque でも同じ API で扱えるようになりました。コレクションの種類を変えてもコードを変えなくてよくなります。

5. String Templates — 文字列テンプレート(Java 21 プレビュー)

※ Java 21 時点ではプレビュー機能です。
正式採用は見送られ、現在(Java25で)も検討中です。
本番コードには使わないことを推奨します。

参考として紹介だけします。
将来こういう書き方ができるかもしれない、という話です。

// 現在は文字列フォーマットに様々な方法がある
var name = "田中";
var age = 30;

// printf スタイル
String.format("名前: %s, 年齢: %d", name, age);

// String Templates(プレビュー・将来の可能性)
// STR."名前: \{name}, 年齢: \{age}"  ← こう書けるかもしれない

実務でJava17→21に上げるときの注意

まず Spring Boot のバージョンを確認

Java 21 の Virtual Threads を Spring Boot で活用するなら、Spring Boot 3.2 以降が必要です。Spring Boot 3 は Jakarta EE 10(jakarta.* 名前空間)ベースです。Spring Boot 2.x のままで Java21 に上げることは不可能ではありませんが、推奨されません。

私の実務では Java8→21 と Spring Boot 2→3 を同時に行いました。
名前空間の変更(javax.*jakarta.*)が一番ボリュームのある作業でしたが、IntelliJ の一括置換を使うとかなりの部分は機械的に処理できます。

(とはいえ、私の実務においては、実際は手動で変更する必要がありました)

synchronized + Virtual Threads のピン留め問題

前述のとおり、既存コードに synchronized が多い場合は注意が必要です。
-Djdk.tracePinnedThreads=full JVM オプションでピン留めが発生している箇所をログに出せるので、Virtual Threads を有効化する前に確認すると安心です。

おわりに

Java は古くてつまらない言語と思われがちです。
しかし振り返ってみると、record 1行のシンプルさ、Virtual Threads のエレガントな解決、パターンマッチングの表現力と、確実に良くなっています。
Java は止まっていません。

一緒にレベルアップを目指していきましょう!

参考

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?