Java には、スレッドを開始したり管理したりするためのメソッドが多数用意されています。この記事では、Java での同時プログラミングのオプションをいくつか紹介します。ここでは構造化同時実行性の概念を紹介し、その後Java 21について説明します。一連のプレビュー クラス - 誤って保留中のタスクを残すことなく、タスクをサブタスクに分割し、結果を収集し、それらに基づいて処理することが非常に簡単になります。
1 基本的な方法
Lambda 式を通じてプラットフォーム スレッドを開始してスレッドを作成するこの方法は、最も単純で単純な状況に適しています。
// Lambda表达式启动平台线程的一种方法。
Thread.ofPlatform().start(() -> {
// 在这里执行在独立线程上运行的操作
});
質問
- プラットフォームスレッドの作成にはコストがかかります
- アプリケーションに多数のユーザーがいる場合、プラットフォーム スレッドの数が JVM でサポートされる制限を超える可能性があります。
どうやら、ほとんどのアプリケーション サーバーはこの動作を推奨していないようです。そこで、次の方法である Java Futures に進みます。
2 Java Future クラス
JDK 5 の導入により、開発者は考え方を変える必要があります。新しいスレッドを開始する代わりに、実行のためにスレッド プールに「タスク」を送信することを考えてください。 JDK 5 では、タスクが送信される ExecutorService も導入されています。 ExecutorService は、タスクを送信して Java Future を返すためのメカニズムを定義するインターフェースです。送信されたタスクは、Runnable または Callable インターフェイスを実装する必要があります。
タスクは単一のスレッドを表すスレッド プールに送信されます。
// 将Callable任务提交给表示单线程线程池的ExecutorService
ExecutorService service = Executors.newSingleThreadExecutor();
Future<String> future = service.submit(() -> {
// 进行一些工作并返回数据
return "Done";
});
// 在这里执行其他任务
// 阻塞直到提交的任务完成
String output = future.get();
// 打印 "Done"
System.out.println(output);
// 继续执行后续任务
ExecutorService に送信された複数のタスク
try (ExecutorService service = Executors.newFixedThreadPool(3)) {
Future<TaskResult> future1 = service.submit(() -> {
// 执行任务1并返回TaskResult
});
Future<TaskResult> future2 = service.submit(() -> {
// 执行任务2并返回TaskResult
});
Future<TaskResult> future3 = service.submit(() -> {
// 执行任务3并返回TaskResult
});
/* 所有异常上抛 */
// get()将阻塞直到任务1完成
TaskResult result1 = future1.get();
// get()将阻塞直到任务2完成
TaskResult result2 = future2.get();
// get()将阻塞直到任务3完成
TaskResult result3 = future3.get();
// 处理result1、result2、result3
handleResults(result1, result2, result3);
}
これらすべてのタスクは並行して実行され、親スレッドは future.get() メソッドを使用して各タスクの結果を取得できます。
3 上記の実装の問題点
上記のコードでプラットフォーム スレッドを使用すると、問題が発生します。 TaskResult の get() メソッドを取得するとスレッドがブロックされますが、プラットフォーム スレッドのブロックに伴うスケーラビリティの問題によりコストがかかる可能性があります。ただし、Java 21 では、仮想スレッドを使用している場合、基礎となるプラットフォーム スレッドは get() 中にブロックされません。
task2 と task3 が task1 よりも前に完了した場合は、task1 が完了するまで待ってから、task2 と task3 の結果を処理する必要があります。
task2 または task3 の実行プロセスが失敗すると、問題はさらに悪化します。いずれかのタスクが失敗するとユースケース全体が失敗すると想定し、コードはタスク 1 が完了するまで待機し、その後例外をスローします。これは私たちが期待しているものではなく、エンド ユーザーにとって非常に遅いエクスペリエンスを生み出すことになります。
3.1 基本的な質問
ExecutorService クラス は、送信されたさまざまなタスク間の関係については何も知りません。したがって、タスクが失敗した場合に何が起こるかわかりません。つまり、例で送信された 3 つのタスクは独立したタスクとみなされ、ユースケースの一部ではありません。 ExecutorService クラスは送信されたタスク間の関係を処理するように設計されていないため、これは ExecutorService クラスの障害ではありません。
3.2 別の質問
ExecutorServicetry-with-resources ブロックの周囲を使用し、試行時に < を呼び出すようにしてください。 block exits /span> メソッドは、実行を続行する前に、executor サービスに送信されたすべてのタスクを確実に終了します。 close の close メソッド。 ExecutorService
タスクが失敗したときに即座に失敗する必要があるユースケースでは、運が悪いです。 close メソッドは、送信されたすべてのタスクが完了するまで待機します。
ただし、try-with-resources ブロックが使用されていない場合、ブロックの前に 3 つのタスクすべてが終了するという保証はありません。出る。クリーンアップせずに終了した「不明確に終了したスレッド」は保持されます。その他のカスタム実装では、失敗時に他のタスクが直ちにキャンセルされるようにする必要があります。
したがって、Java Futures の使用はサブタスクに分割できるタスクを処理する良い方法ですが、まだ完璧ではありません。開発ではユースケースの「認識」をロジックにエンコードする必要がありますが、それは難しいです。
Java Futures のプラットフォーム スレッドの問題の 1 つはブロッキング問題であることに注意してください。この問題は、Java 21 の仮想スレッドを使用する場合には存在しません。仮想スレッドを使用する場合、future.get() メソッドを使用してスレッドをブロックすると、基盤となるプラットフォーム スレッドが解放されるためです。
使用CompletableFutureパイプラインでもブロックの問題を解決できますが、ここでは詳しく説明しません。 Java 21 のブロッキング問題を解決する簡単な方法があります。それは、仮想スレッドです。しかし、サブタスクに分割でき、ユースケースを「知っている」タスクを処理するためのより良いソリューションを見つける必要があります。これは構造化された同時実行性の基本的な考え方につながります。
4 構造化された同時実行性
タスクがメソッド内から ExecutorService に送信され、メソッドが終了すると想像してください。この送信されたタスクの潜在的な副作用が不明であるため、コードを推論することが難しくなり、デバッグが難しい問題が発生する可能性があります。問題の図:
構造化同時実行基本的な考え方は、ブロック (メソッドまたはブロック) 内で開始されたすべてのタスクは、ブロックの終了前に終了する必要があるということです。つまり:
コードの構造境界 (ブロック)
およびブロック内で送信されたタスクの実行時境界
一致。これにより、ブロック内で送信されたすべてのタスクの実行効果がそのブロックに制限されるため、アプリケーション コードが理解しやすくなります。ブロックの外側のコードを表示するときは、タスクがまだ実行中かどうかを心配する必要はありません。
ExecutorService のtry-with-resources ブロックは構造化された同時実行性< を目的としています。 i=4> を試行します。ブロック内から送信されたすべてのタスクは、ブロックの終了時に完了します。しかし、親スレッドが必要以上に長く待機する可能性があるため、これだけでは十分ではありません。その改良版 - StructuredTaskScope。
5 構造化タスクスコープ
Java 21 仮想スレッドは、ほとんどの場合のブロッキング問題を事実上排除する機能として導入されました。しかし、仮想スレッドやフューチャーを使用しても、「タスクの不潔な終了」や「必要以上に長い待機」といった問題は依然として存在します。
StructuredTaskScope クラスは、この問題を解決するために Java 21 のプレビュー機能として提供されています。 Executor Service の try-with-resources ブロック クラスは、送信されたタスク間の関係を認識しているため、タスクについてより賢明な仮定を立てることができます。 StructuredTaskScope 構造化された同時実行性
使用例StructuredTaskScope
タスクが失敗した場合は、すぐにユースケースに戻ります。
StructuredTaskScope.ShutdownOnFailure() StructuredTaskScope への参照を返します。StructuredTaskScope は、送信されたタスク間の関係を「認識」しているため、1 つのタスクが失敗した場合、他のタスクも終了する必要があることを認識しています。
try(var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 想象一下LongRunningTask实现Supplier
var dataTask = new LongRunningTask("dataTask", ...);
var restTask = new LongRunningTask("restTask", ...);
// 并行运行任务
Subtask<TaskResponse> dataSubTask = scope.fork(dataTask);
Subtask<TaskResponse> restSubTask = scope.fork(restTask);
// 等待所有任务成功完成或第一个子任务失败。
// 如果一个失败,向所有其他子任务发送取消请求
// 在范围上调用join方法,等待两个任务都完成或如果一个任务失败
scope.join();
scope.throwIfFailed();
// 处理成功的子任务结果
System.out.println(dataSubTask.get());
System.out.println(restSubTask.get());
}
エンタープライズでの使用例
次の 2 つのタスクは並行して実行できます。
- DBタスク
- Rest API タスク
目標は、これらのタスクを並行して実行し、結果を 1 つのオブジェクトに結合して返すことです。
呼び出しShutdownOnFailure() 静的メソッドを作成してStructuredTaskScope 親切です。次に、StructuredTaskScopeobjectfork メソッドを使用します (< を置き換えます) a i =9>fork メソッドは、2 つのタスクを並行して実行するための submit メソッドとみなされます。バックグラウンドでStructuredTaskScope クラスは、デフォルトで仮想スレッドを使用してタスクを実行します。タスクをフォークするたびに、新しい仮想スレッドを作成し (仮想スレッドがプールされることはありません)、タスクを実行します。
次に、スコープでjoin メソッドを呼び出し、両方のタスクが完了するか、一方のタスクが失敗するまで待機します。さらに重要なことは、タスクが失敗した場合、join() メソッドは自動的にキャンセル リクエストを他のタスク (残りの実行中のタスク) に送信し、それらのタスクが終了するまで待機することです。 。リクエストをキャンセルすると、ブロックの終了時に不要な保留中のタスクがなくなるため、これは重要です。
他のスレッドがキャンセル リクエストを親スレッドに送信する場合も同様です。最後に、ブロック内のどこかで例外がスローされた場合、StructuredTaskScope の close メソッドにより、キャンセル リクエストがサブタスクに送信され、タスクは終了します。 StructuredTaskScope優れている点は、子スレッドが独自の StructuredTaskScope を作成する場合です (サブタスク自体には独自のサブタスクがあります)、キャンセルされたときにそれらはすべてきれいに処理されます。
ここでの開発者の責任の 1 つは、作成したタスクがキャンセル中にスレッドに設定される割り込みフラグを正しく処理することを保証することです。この割り込みフラグを読み取り、タスク自体を正常に終了するのはタスクの責任です。タスクが割り込みフラグを正しく処理しない場合、ユースケースの応答性に影響します。
6 使用StructuredTaskScope
ユースケースでタスクをサブタスクに分解する必要があり、場合によってはサブタスクをさらに多くのサブタスクに分解する必要がある場合は、StructuredTaskScope が適切です。この記事の例は、サブタスクが失敗した場合にすぐに戻る必要があるユースケースです。しかし、StructuredTaskScope はそれだけではありません。
- 最初のタスクが成功したときに返されます
- すべてのタスクが完了すると返されます (成功または失敗)。
- 独自のバージョンの StructuredTaskScope を作成する
6.1 StructuredTaskScope の利点
- ユースケースに関係なくコードが同じに見えるため、コードが読みやすい
- 失敗した子スレッドは、必要に応じて正常に終了されます。無駄な糸が垂れない
- 仮想スレッドとStructuredTaskScope を併用すると、ブロックに関連するスケーラビリティの問題が存在しません。デフォルトで、StructuredTaskScope が内部で仮想スレッドを使用するのも不思議ではありません。
7 まとめ
全体として、StructuredTaskScope クラスは、タスクを複数のサブタスクに分割するユースケースを処理するために Java に追加するのに適しています。障害時の子スレッドの自動キャンセル、さまざまなユースケースでのコードの一貫性、およびコードをより深く理解できる機能により、Java で構造化同時実行を実装するのは理想的な選択肢となります。
仮想スレッドとStructuredTaskScope クラスは、完璧な組み合わせを形成します。仮想スレッドを使用すると、JVM 内に数十万のスレッドを作成でき、StructuredTaskScope クラスを使用すると、これらのスレッドを効率的に管理できます。
プレビューが終了して正式な機能になるのを待ちましょう!
この記事は、ブログ投稿プラットフォーム OpenWrite によって公開されています。