Red Hat のソリューションアーキテクトの瀬戸です。
この記事はRed Hat Developerのブログ記事、What's new for developers in JDK 21 | Red Hat Developer を、許可をうけて翻訳したものです。
Java開発者にとってエキサイティングな情報として、今年 9 月 19 日に JDK 21 がリリースされました。 このリリースには、仮想スレッド(Virtual Thread)、レコードパターン(Record Patterns)、順序付コレクション(Sequenced Collections)など、Javaのエコシステムに利益をもたらす多くの新機能が含まれています。JDK 21 のプレビューには、文字列テンプレート(String Templates)、スコープ付値(Scoped Values)、構造化並列処理(Structured Concurrency)など、いくつかの興味深い機能もあります。この記事では、このリリースの 6 つの新機能について説明します。
仮想スレッド Virtual Thread
Java の従来のスレッドモデルではアプリケーションがオペレーティング システム (OS) が処理できる以上のスレッドを作成すると即座に高コストの操作になる可能性があります。 また、スレッドの生存期間が長くない場合はスレッド作成のためのコストのオーバーヘッドが大きくなります。
仮想スレッドが導入されると、Java スレッドをキャリアスレッドにマッピングし、キャリアスレッドへのスレッド操作を管理 (つまりマウント/アンマウント) することで、この問題を解決します。キャリアスレッドは OS スレッドと連携して動作します。これは開発者に柔軟性と制御する余地を与える抽象化です。図 1 を参照してください。
以下は仮想スレッドの例であり、OS/プラットフォームのスレッドと比べた例です。プログラムは ExecutorService
を使用して10,000 個のタスクを作成し、それらがすべて完了するまで待機します。JDK はこれらをバックグラウンドで限られた数の キャリア/OS スレッドで実行し、並列処理コードを簡単に作成できる安定性を提供します。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); }); } // executor.close() is called implicitly, and waits
※訳注: スレッド作成のオーバーヘッドが強調されていますが、主な導入メリットはI/O操作が行われた時に実質的にスレッドでの処理が止まってしまい無駄なリソースが発生してしまうため、その余剰リソースを無駄にしないようになります。
構造化並列処理 Structured concurrency (Preview)
構造化並列処理は仮想スレッドと密接に関係しており、キャンセル、シャットダウン、スレッド リークなどの一般的なリスクを排除する開発者のエクスペリエンスを向上させる API を提供することを目的としています。タスクが並列実行されるサブタスクに分割された場合、それらはすべて同じ場所、つまりタスクのコード ブロックに戻る必要があります。
図 2 では、findUser
と fetchOrder
の両方を実行してそれぞれのサービスからデータを取得し、両方のデータを使用して結果を作成し、コンシューマーに応答として送り返す必要があります。通常、これらのタスクは並列に処理できますが、 findUser
が結果を返さないといったエラーが発生しやすく、 fetchOrder
は他の処理が完了するまで待ってから、最後に結合操作を実行する必要があります。
さらに、サブタスクの生存期間は親タスク自体の生存期間を超えることは出来ません。複数スレッドで非同期に実行される複数の高速な実行 I/O 操作の結果を合成するタスク操作を想像してください。構造化並列処理モデルは、仮想スレッド API と StructuredTaskScope
を活用することで、スレッド プログラミングをシングル スレッド の 書き方の簡単さに近づけます。
Response handle() throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Supplier<String> user = scope.fork(() -> findUser()); Supplier<Integer> order = scope.fork(() -> fetchOrder()); scope.join() // Join both subtasks .throwIfFailed(); // ... and propagate errors // Here, both subtasks have succeeded, so compose their results return new Response(user.get(), order.get()); } }
スコープ付値 Scoped values (Preview)
スコープ付値 は、 スレッドを使用したプログラミング時の生産性にも面白い変更をもたらします。
歴史的に、Java 開発者は、スレッド中の呼び出しチェーンを通して同じデータにアクセスするために、ThreadLocal
を使用してきました。
しかしながら、ThreadLocal変数を使用することで呼び出しチェーンを通してデータを変更するのも簡単に行えます。
そのため、プログラミングが難しくなり、時にはエラーが発生しやすくなり、安全でないコードになる危険性があります。
ScopeValue
はスレッドがスコープ内でデータを変更できないようにしたデータを共有できるモデルを提供することでこの問題を解決することを目指しています。ScopeValue
のデータは不変であり実行時の最適化が可能になります。
セキュリティ プリンシパル、トランザクション、共有コンテキストを使用するマルチスレッドアプリケーションはスコープ付値の恩恵を受けます。
次のScopeValue<String>
の例は、 runWhere
のスコープで作成および使用される実行可能なメソッドを示しています。
public class WithUserSession { // Creates a new ScopedValue private final static ScopedValue<String> USER_ID = new ScopedValue.newInstance(); public void processWithUser(String sessionUserId) { // sessionUserId is bound to the ScopedValue USER_ID for the execution of the // runWhere method, the runWhere method invokes the processRequest method. ScopedValue.runWhere(USER_ID, sessionUserId, () -> processRequest()); } // ... }
順序付コレクション Sequenced collections
JDK 21では、コレクションの使用エクスペリエンスを向上させるために、新しいコレクション インターフェイスが導入されています (図 3)。たとえば、使用中のコレクションによっては、コレクションから要素を逆の順序で取得することが面倒な作業になる可能性があります。使用されているコレクションによっては、逆順の取得に不一致が発生する可能性があります。たとえば、 SortedSet
は順序を実装していますが HashSet
は順序を実装していないので、異なるデータセットでこれを実現するのは面倒です。
※訳注: 図ではSequencedSetがSequencedCollectionを継承していないように見えますが、実際には継承しています。
これを解決するために SequencedCollection
インターフェースに逆順で取得するメソッドや最初と最後の要素を取得する機能を追加してアクセス順序を補助します。
さらに SequencedMap
と SequencedSet
インターフェースもあります。
interface SequencedCollection<E> extends Collection<E> { // new method SequencedCollection<E> reversed(); // methods promoted from Deque void addFirst(E); void addLast(E); E getFirst(); E getLast(); E removeFirst(); E removeLast(); }
そのため、アクセス順序に一貫性を持たせるだけでなく、最初と最後の要素を削除したり追加したりすることもできるようになりました。
レコードパターン Record patterns
レコードは Java 14 でプレビューとして導入され、Java 列挙型も提供されました。record
は Java の特殊な型であり、データ キャリアとしてのみ機能するクラスの開発プロセスを容易にするために実装されました。
JDK 21 では、レコードパターンと型パターンをネストして宣言的で構造的な形式のデータ ナビゲーションと処理を可能にすることができます。
// To create a record: Public record Todo(String title, boolean completed){} // To create an Object: Todo t = new Todo(“Learn Java 21”, false);
JDK 21 以前では、アクセサーを扱うにはレコード全体にアクセスする必要がありました。しかし、現在では値の取得がはるかに簡素化されています。例えば次のようになります:
static void printTodo(Object obj) { if (obj instanceof Todo(String title, boolean completed)) { System.out.print(title); System.out.print(completed); } }
レコード パターンのもう 1 つの利点は、ネストされたレコードとそれ自体へのアクセスです。JEP 定義での例は、 Rectangle
にネストされている ColoredPoint
の一部の値を取得する機能を示しています。これにより、毎回すべてのアクセサーを経由していく必要があった以前よりもはるかに便利になります。
// As of Java 21 static void printColorOfUpperLeftPoint(Rectangle r) { if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) { System.out.println(c); } }
文字列テンプレート String templates (Preview)
文字列テンプレートはJDK 21 ではプレビュー機能です。しかし、インジェクションなどの望ましくない結果につながる可能性がある一般的な落とし穴を回避できるため、String
の操作の信頼性とエクスペリエンスが向上します。テンプレート式を書いて、それをString
でレンダリングできるようになりました。
// As of Java 21 String name = "Shaaf" String greeting = STR."Hello \{name}"; System.out.println(greeting);
この例では2 行目は式であり、呼び出すと Hello Shaaf
が表示されるはずです。
さらに、不正な String
(SQL文やセキュリティ問題を引き起こす可能性のあるHTMLなど)が存在する可能性がある場合、テンプレートのルールはエスケープされた引用符のみを許可し、HTMLドキュメント内の不正なエンティティを許可しません。
※訳注: 元の文章でSTR.が抜けていたのを直しました。
Java のサポートを受けるには
OpenJDK と Eclipse Temurin のサポートはRed Hat RuntimesやRed Hat Enterprise Linuxおよび Red Hat OpenShift のサブスクリプションを通じて Red Hat のお客様に提供されます。 詳細については、お近くの Red Hat 代理店または Red Hat 営業にお問い合わせください。Java やその他のランタイムのサポートポリシーは、Red Hat Application Services 製品のアップデートとサポートポリシーに記載されています。
※訳注: JBoss EAPを含むRed Hat RuntimesはWindows上でも動作します。また、Windows上でのOpenJDK単体のリーズナブルなサポートや、Oracle Javaからの移行に対するご相談も受け付けておりますのでお問い合わせください。