フォーク/ジョインの概要
Fork/Joinフレームワークは、JDK 7から導入されました。名前が示すように、並列コンピューティングフレームワークです。ForkとJoinの2つの操作に基づいており、その機能は、大きなタスクを複数の小さなタスクに分割し、それらを分割することです。小さなタスク。タスクを結合して大きなタスクの結果を取得します。Fork/Join アーキテクチャを使用すると、マルチコア CPU のパフォーマンス上の利点を最大限に活用し、プログラムの実行効率を向上させることができます。
フォーク/結合コンポーネント
Fork/Join フレームワークには主に 3 つのコンポーネントが含まれています。
- スレッド プール: ForkJoinPool はタスクを並列実行するためのスレッド プールを維持し、必要に応じてスレッドの数を動的に調整できます。
- Task オブジェクト: ForkJoinTask は、分割可能なタスクを表す抽象クラスです。ForkJoinTask のサブクラス (RecursiveAction および RecursiveTask) は、fork() メソッドと compute() メソッドを実装する必要があります。fork() メソッドはタスクを分割してサブタスクの結果を返すために使用され、compute() メソッドは実行のために使用されます。タスクの実際の計算。
- タスクを実行するスレッド: ForkJoinWorkerThread は、タスクの実行を担当する ForkJoinPool のワーカー スレッドです。
フォーク/ジョインの原則 - 分割統治法
ForkJoinPool は主に、分割統治アルゴリズムを使用して問題を解決するために使用されます。クイック ソート アルゴリズムや ForkJoinPool などの一般的なアプリケーションでは、多数のタスクを処理するために比較的少数のスレッドを使用する必要があります。たとえば、1,000 万個のデータを並べ替える場合、このタスクは 500 万個の 2 つの並べ替えタスクと、これら 2 セットの 500 万個のデータのマージ タスクに分割されます。同様に、500万件のデータに対して同様の分割処理を行い、最終的にデータサイズがどのくらいになったら分割処理を停止するかという閾値を設定します。たとえば、要素数が 10 未満の場合、分割は停止され、挿入ソートが使用されて並べ替えられます。したがって、最終的にはすべてのタスクを合計すると約 2,000,000 以上になります。問題の鍵は、タスクの場合、そのすべてのサブタスクが完了した後でのみ実行できることです。
フォーク/ジョインの原理による作業盗用アルゴリズム
Fork/Join の中核は、マルチコアの最新のハードウェア デバイスを使用することです。操作中、アイドル状態の CPU が存在します。したがって、このアイドル状態の CPU をどのように活用するかがパフォーマンスを向上させる鍵となります。 (ワーク スチール) アルゴリズムは、Fork/Join フレームワーク全体の中心的な概念です。Fork/Join ワーク スチール (ワーク スチール) アルゴリズムは、実行のために他のキューからタスクを盗むスレッドを指します。
なぜ仕事盗用アルゴリズムを使用する必要があるのでしょうか? 比較的大きなタスクを実行する必要がある場合は、タスクを互いに独立した多数のサブタスクに分割し、スレッド間の競合を減らすために、これらのサブタスクを異なるキューに入れて各キューに割り当てます。キュー内のタスクを実行する別のスレッドを作成します。スレッドとキューは 1 対 1 に対応します。たとえば、スレッド A はキュー A 内のタスクの処理を担当します。ただし、一部のスレッドは自分のキュー内のタスクを最初に終了しますが、他のスレッドに対応するキュー内には処理を待っているタスクがまだ存在します。作業を終えたスレッドは、待機する代わりに、他のスレッドの作業を手助けすることもできるため、他のスレッドのキューからタスクを盗んで実行します。このとき、同じキューにアクセスすることになるため、盗用タスクスレッドと盗用タスクスレッドとの競合を減らすために、通常は両端キューが使用され、盗まれたタスクスレッドは常に先頭からタスクを取得します。実行用の両端キュー タスクを盗むスレッドは、常に両端キューの最後尾からタスクを取得して実行します。
ワークスチールアルゴリズムの利点は、スレッドを最大限に活用して並列計算を行い、スレッド間の競合を減らすことですが、欠点は、依然として競合が存在する場合があることです。前述したように、自動並列化の概念は Java 8 で導入されました。Java コードの一部を自動的に並列実行できるようにします。つまり、ForkJoinPool の ParallelStream を使用します。
ForkJoinPool の一般スレッド プールのスレッド数には、通常、ランタイム コンピューターのプロセッサの数である既定値を使用するだけで十分です。システム プロパティ: java.util.concurrent.ForkJoinPool.common.Parallelism=N (N はスレッド数) を設定することで、ForkJoinPool のスレッド数を調整できます。さまざまなパラメータに調整して、それぞれの出力結果を観察してみてください。時間。
フォーク/ジョインの場合
Fork/Joinを使用して1~10000の合計を計算します。タスクの計算回数が3000を超える場合はタスクを分割し、3000未満の場合は計算されます。
public class Test05 {
* @param args
*/
public static void main(String[] args) {
long start = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
SumRecursiveTask task = new SumRecursiveTask(1,10000l);
Long result = pool.invoke(task);
System.out.println("result="+result);
long end = System.currentTimeMillis();
System.out.println("总的耗时:" + (end-start));
}
}
class SumRecursiveTask extends RecursiveTask<Long>{
// 定义一个拆分的临界值
private static final long THRESHOLD = 3000l;
private final long start;
private final long end;
public SumRecursiveTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end -start;
if(length <= THRESHOLD){
// 任务不用拆分,可以计算
long sum = 0;
for(long i=start ; i <= end ;i++){
sum += i;
}
System.out.println("计算:"+ start+"-->" + end +",的结果为:" + sum);
return sum;
}else{
// 数量大于预定的数量,那说明任务还需要继续拆分
long middle = (start+end)/2;
System.out.println("拆分:左边 " + start+"-->" + middle+", 右边" + (middle+1) + "-->" + end);
SumRecursiveTask left = new SumRecursiveTask(start, middle);
left.fork();
SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
right.fork();
return left.join()+right.join();
}
}
}