JavaのFork / Joinフレームワークのガイド
1. 概要
fork / joinフレームワークはJava 7で提示されました。 利用可能なすべてのプロセッサコアの使用を試みることにより、並列処理を高速化するのに役立つツールを提供します。これはthrough a divide and conquer approachで達成されます。
実際には、これはthe framework first “forks”が、非同期で実行できるほど単純になるまで、タスクをより小さな独立したサブタスクに再帰的に分割することを意味します。
その後、すべてのサブタスクの結果が再帰的に1つの結果に結合されるthe “join” part begins、またはvoidを返すタスクの場合、プログラムはすべてのサブタスクが実行されるまで待機します。
効果的な並列実行を提供するために、fork / joinフレームワークは、ForkJoinWorkerThreadタイプのワーカースレッドを管理するForkJoinPoolと呼ばれるスレッドのプールを使用します。
2. ForkJoinPool
ForkJoinPoolはフレームワークの中心です。 これは、ワーカースレッドを管理し、スレッドプールの状態とパフォーマンスに関する情報を取得するためのツールを提供するExecutorServiceの実装です。
ワーカースレッドは一度に1つのタスクしか実行できませんが、ForkJoinPoolはサブタスクごとに個別のスレッドを作成するわけではありません。 代わりに、プール内の各スレッドには、タスクを格納する独自の両端キュー(またはdeque、発音deck)があります。
このアーキテクチャは、work-stealing algorithm.の助けを借りてスレッドのワークロードのバランスを取るために不可欠です
2.1. 仕事を盗むアルゴリズム
簡単に言えば、フリースレッドはビジースレッドの両端キューから作業を「盗もう」とします。
デフォルトでは、ワーカースレッドは自身のdequeのヘッドからタスクを取得します。 空の場合、スレッドは別のビジースレッドの両端キューの末尾またはグローバルエントリキューからタスクを取得します。これは、最も大きな作業が行われる可能性がある場所だからです。
このアプローチにより、スレッドがタスクをめぐって競合する可能性が最小限に抑えられます。 また、最初に利用可能な最大のチャンクで動作するため、スレッドが作業を探しに行く回数を減らします。
2.2. ForkJoinPool インスタンス化
Java 8では、ForkJoinPoolのインスタンスにアクセスするための最も便利な方法は、静的メソッドcommonPool().を使用することです。その名前が示すように、これは共通プールへの参照を提供します。 ForkJoinTaskごとのデフォルトのスレッドプール。
Oracle’s documentationによると、事前定義された共通プールを使用すると、タスクごとに個別のスレッドプールを作成できなくなるため、リソースの消費が削減されます。
ForkJoinPool commonPool = ForkJoinPool.commonPool();
Java 7でも、ForkJoinPoolを作成し、それをユーティリティクラスのpublic staticフィールドに割り当てることで、同じ動作を実現できます。
public static ForkJoinPool forkJoinPool = new ForkJoinPool(2);
簡単にアクセスできるようになりました:
ForkJoinPool forkJoinPool = PoolUtil.forkJoinPool;
ForkJoinPool’sコンストラクターを使用すると、特定のレベルの並列処理、スレッドファクトリ、および例外ハンドラーを使用してカスタムスレッドプールを作成できます。 上記の例では、プールの並列度レベルは2です。 これは、プールが2つのプロセッサコアを使用することを意味します。
3. ForkJoinTask<V>
ForkJoinTaskは、ForkJoinPool.内で実行されるタスクの基本タイプです。実際には、その2つのサブクラスの1つを拡張する必要があります。voidタスクのRecursiveActionとRecursiveTask<V>値を返すタスクの場合。 タスクのロジックが定義されている_ They both have an abstract method _compute()。
3.1. RecursiveAction – An Example
以下の例では、処理される作業単位は、workloadと呼ばれるStringで表されます。 デモンストレーションのために、タスクは無意味なものです。入力を単に大文字にしてログに記録します。
フレームワークのフォーク動作を示すために、the example splits the task if workload.length() is larger than a specified threshold_ using the _createSubtask()メソッド。
文字列は再帰的に部分文字列に分割され、これらの部分文字列に基づくCustomRecursiveTaskインスタンスが作成されます。
その結果、メソッドはList<CustomRecursiveAction>.を返します
リストは、invokeAll()メソッドを使用してForkJoinPoolに送信されます。
public class CustomRecursiveAction extends RecursiveAction {
private String workload = "";
private static final int THRESHOLD = 4;
private static Logger logger =
Logger.getAnonymousLogger();
public CustomRecursiveAction(String workload) {
this.workload = workload;
}
@Override
protected void compute() {
if (workload.length() > THRESHOLD) {
ForkJoinTask.invokeAll(createSubtasks());
} else {
processing(workload);
}
}
private List createSubtasks() {
List subtasks = new ArrayList<>();
String partOne = workload.substring(0, workload.length() / 2);
String partTwo = workload.substring(workload.length() / 2, workload.length());
subtasks.add(new CustomRecursiveAction(partOne));
subtasks.add(new CustomRecursiveAction(partTwo));
return subtasks;
}
private void processing(String work) {
String result = work.toUpperCase();
logger.info("This result - (" + result + ") - was processed by "
+ Thread.currentThread().getName());
}
}
このパターンを使用して、独自のRecursiveActionクラス.を開発できます。これを行うには、作業の合計量を表すオブジェクトを作成し、適切なしきい値を選択し、作業を分割する方法を定義し、定義します。仕事をする方法。
3.2. RecursiveTask<V>
値を返すタスクの場合、各サブタスクの結果が単一の結果に統合されることを除いて、ここのロジックは似ています。
public class CustomRecursiveTask extends RecursiveTask {
private int[] arr;
private static final int THRESHOLD = 20;
public CustomRecursiveTask(int[] arr) {
this.arr = arr;
}
@Override
protected Integer compute() {
if (arr.length > THRESHOLD) {
return ForkJoinTask.invokeAll(createSubtasks())
.stream()
.mapToInt(ForkJoinTask::join)
.sum();
} else {
return processing(arr);
}
}
private Collection createSubtasks() {
List dividedTasks = new ArrayList<>();
dividedTasks.add(new CustomRecursiveTask(
Arrays.copyOfRange(arr, 0, arr.length / 2)));
dividedTasks.add(new CustomRecursiveTask(
Arrays.copyOfRange(arr, arr.length / 2, arr.length)));
return dividedTasks;
}
private Integer processing(int[] arr) {
return Arrays.stream(arr)
.filter(a -> a > 10 && a < 27)
.map(a -> a * 10)
.sum();
}
}
この例では、作業はCustomRecursiveTaskクラスのarrフィールドに格納されている配列で表されます。 createSubtask()メソッドは、各部分がしきい値より小さくなるまで、タスクを再帰的に小さな部分に分割します。.次に、invokeAll()メソッドは、サブタスクを共通のプルに送信し、%のリストを返します。 (t5)s。
実行をトリガーするために、join()メソッドが各サブタスクを呼び出しました。
この例では、これはJava 8のStream API;を使用して実行されます。sum()メソッドは、サブ結果を最終結果に結合する表現として使用されます。
4. ForkJoinPoolへのタスクの送信
スレッドプールにタスクを送信するには、いくつかのアプローチを使用できます。
submit()またはexecute()メソッド(これらのユースケースは同じです):
forkJoinPool.execute(customRecursiveTask);
int result = customRecursiveTask.join();
invoke()メソッドはタスクをフォークして結果を待ち、手動で結合する必要はありません。
int result = forkJoinPool.invoke(customRecursiveTask);
invokeAll()メソッドは、ForkJoinTasksのシーケンスをForkJoinPool.に送信するための最も便利な方法です。タスクをパラメーター(2つのタスク、var args、またはコレクション)として受け取り、フォークして、生成された順序でのFutureオブジェクトのコレクション。
または、個別のfork() and join()メソッドを使用することもできます。 fork()メソッドはタスクをプールに送信しますが、その実行はトリガーされません。 この目的には、join()メソッドが使用されます。 RecursiveActionの場合、join()はnullのみを返します。 RecursiveTask<V>,の場合、タスクの実行結果を返します。
customRecursiveTaskFirst.fork();
result = customRecursiveTaskLast.join();
RecursiveTask<V>の例では、invokeAll()メソッドを使用して、一連のサブタスクをプールに送信しました。 同じジョブをfork()とjoin()で実行できますが、これは結果の順序に影響します。
混乱を避けるために、通常、invokeAll()メソッドを使用して複数のタスクをForkJoinPool.に送信することをお勧めします。
5. 結論
fork / joinフレームワークを使用すると、大きなタスクの処理を高速化できますが、この結果を達成するには、いくつかのガイドラインに従う必要があります。
-
Use as few thread pools as possible –ほとんどの場合、最良の決定は、アプリケーションまたはシステムごとに1つのスレッドプールを使用することです。
-
特定の調整が必要ない場合はUse the default common thread pool,
-
ForkJoingTaskをサブタスクに分割するためのUse a reasonable threshold
-
あなたのブロッキングを避けてください ForkJoingTasks
この記事で使用されている例は、linked GitHub repositoryで利用できます。