Java 8並列ストリームのカスタムスレッドプール


398

Java 8 並列ストリームのカスタムスレッドプールを指定することは可能ですか?どこにも見つかりません。

サーバーアプリケーションがあり、並列ストリームを使用したいとします。しかし、アプリケーションは大きく、マルチスレッドなので、それを区分けしたいと思います。別のモジュールからのapplicationblockタスクの1つのモジュールで実行速度の遅いタスクが必要ではありません。

異なるモジュールに異なるスレッドプールを使用できない場合、それは実際の状況のほとんどで並列ストリームを安全に使用できないことを意味します。

次の例を試してください。別のスレッドで実行されるCPU集中タスクがいくつかあります。タスクは並列ストリームを活用します。最初のタスクは壊れているため、各ステップには1秒かかります(スレッドスリープによってシミュレートされます)。問題は、他のスレッドがスタックして、壊れたタスクが完了するのを待つことです。これは不自然な例ですが、サーブレットアプリと誰かが共有フォーク結合プールに長時間実行タスクを送信するとします。

public class ParallelTest {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newCachedThreadPool();

        es.execute(() -> runTask(1000)); //incorrect task
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));
        es.execute(() -> runTask(0));


        es.shutdown();
        es.awaitTermination(60, TimeUnit.SECONDS);
    }

    private static void runTask(int delay) {
        range(1, 1_000_000).parallel().filter(ParallelTest::isPrime).peek(i -> Utils.sleep(delay)).max()
                .ifPresent(max -> System.out.println(Thread.currentThread() + " " + max));
    }

    public static boolean isPrime(long n) {
        return n > 1 && rangeClosed(2, (long) sqrt(n)).noneMatch(divisor -> n % divisor == 0);
    }
}

3
カスタムスレッドプールとはどういう意味ですか?共通のForkJoinPoolは1つだけですが、いつでも独自のForkJoinPoolを作成してリクエストを送信できます。
2014年

7
ヒント:JavaチャンピオンのHeinz Kabutzが同じ問題を調査しますが、さらに悪い影響があります。共通のフォーク結合プールのスレッドをデッドロックすることです。javaspecialists.eu/archive/Issue223.htmlを
Peti

回答:


395

実際には、特定のfork-joinプールで並列操作を実行する方法があります。fork-joinプールでタスクとして実行すると、そこにとどまり、共通のものを使用しません。

final int parallelism = 4;
ForkJoinPool forkJoinPool = null;
try {
    forkJoinPool = new ForkJoinPool(parallelism);
    final List<Integer> primes = forkJoinPool.submit(() ->
        // Parallel task here, for example
        IntStream.range(1, 1_000_000).parallel()
                .filter(PrimesPrint::isPrime)
                .boxed().collect(Collectors.toList())
    ).get();
    System.out.println(primes);
} catch (InterruptedException | ExecutionException e) {
    throw new RuntimeException(e);
} finally {
    if (forkJoinPool != null) {
        forkJoinPool.shutdown();
    }
}

このトリックはForkJoinTask.forkに基づいており、「該当する場合は、現在のタスクが実行されているプールでこのタスクを非同期で実行するか、またはinForkJoinPool()でない場合はForkJoinPool.commonPool()を使用するようにします」


20
ソリューションの詳細については、ここで説明されているblog.krecan.net/2014/03/18/...
ルーカス・

3
しかし、ストリームがを使用することも指定さForkJoinPoolれているのですか、それとも実装の詳細ですか ドキュメントへのリンクがいいでしょう。
Nicolai

6
@Lukasスニペットをありがとう。スレッドリークを回避するためにForkJoinPoolインスタンスがshutdown()不要になったときにインスタンスを追加する必要があります。(例)
jck 2015

5
Java 8にはバグがあり、タスクがカスタムプールインスタンスで実行されている場合でも、それらは共有プールに結合されます。計算のサイズは、カスタムプールではなく共通プールに比例します。Java 10で修正されました:JDK-8190974
Terran

3
@terranこの問題は、Java 8のバグで
browse /

192

並列ストリームはデフォルトForkJoinPool.commonPoolで使用します。デフォルトではプロセッサがあるためスレッドが1つ少なくなります。Runtime.getRuntime().availableProcessors()これは、並列ストリームはメインスレッドも使用するため、すべてのプロセッサを使用することを意味します。

個別のプールまたはカスタムプールを必要とするアプリケーションの場合、ForkJoinPoolは特定のターゲット並列処理レベルで構築できます。デフォルトでは、使用可能なプロセッサーの数と同じです。

これは、ネストされた並列ストリームまたは同時に開始された複数の並列ストリームがある場合、それらはすべて同じプールを共有することも意味します。利点:デフォルト(使用可能なプロセッサーの数)を超えて使用することはありません。短所:開始する各並列ストリームに「すべてのプロセッサ」が割り当てられない場合があります(たまたま複数ある場合)。(どうやら、ManagedBlockerを使用してそれを回避できます。)

並列ストリームの実行方法を変更するには、次のいずれかを実行できます

  • 並列ストリーム実行を独自のForkJoinPoolに送信します。yourFJP.submit(() -> stream.parallel().forEach(soSomething)).get();または
  • システムプロパティを使用して共通プールのサイズを変更できますSystem.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20")。ターゲットの並列処理が20スレッドの場合。ただし、これは、バックポートされたパッチhttps://bugs.openjdk.java.net/browse/JDK-8190974の後で機能しなくなります

8つのプロセッサーを搭載した私のマシンでの後者の例。次のプログラムを実行すると:

long start = System.currentTimeMillis();
IntStream s = IntStream.range(0, 20);
//System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20");
s.parallel().forEach(i -> {
    try { Thread.sleep(100); } catch (Exception ignore) {}
    System.out.print((System.currentTimeMillis() - start) + " ");
});

出力は次のとおりです。

215216216216216216216216216315316316316316316316316316415415416416416

したがって、並列ストリームが一度に8つの項目を処理する、つまり8つのスレッドを使用することがわかります。ただし、コメント行をコメント解除すると、出力は次のようになります。

215215215215215216216216216216216216216216216216216216216216216216

今回、並列ストリームは20のスレッドを使用し、ストリーム内の20の要素すべてが同時に処理されました。


30
commonPoolは実際には1より小さいavailableProcessors値がありavailableProcessors、呼び出しスレッドが1としてカウントされるため、合計並列度はと等しくなります。
Marko Topolnik 14

2
返品を提出してくださいForkJoinTask。模倣するparallel() get()必要がある:stream.parallel().forEach(soSomething)).get();
グリゴリーKislin

5
ForkJoinPool.submit(() -> stream.forEach(...))指定されたでStreamアクションが実行されるとは確信していませんForkJoinPool。Stream-Action全体が1つのアクションとしてForJoinPoolで実行されることを期待しますが、内部的には依然としてデフォルト/共通のForkJoinPoolを使用しています。どこで見ましたか、ForkJoinPool.submit()はあなたが言うことを実行しますか?
フレデリックライテンベルガー、

@FredericLeitenbergerおそらくルーカスの回答の下にコメントを置くつもりでした。
アッシリア

2
私は今、stackoverflow.com / a / 34930831/1520422が発表どおりに実際に機能していることをうまく示しています。それでも私はそれがどのように機能するかをまだ理解していません。しかし、「うまくいく」で大丈夫です。ありがとう!
フレデリックライテンベルガー、

39

独自のforkJoinPool内で並列計算をトリガーするトリックの代わりに、次のようにそのプールをCompletableFuture.supplyAsyncメソッドに渡すこともできます。

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
CompletableFuture<List<Integer>> primes = CompletableFuture.supplyAsync(() ->
    //parallel task here, for example
    range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList()), 
    forkJoinPool
);

22

元のソリューション(ForkJoinPoolの共通並列処理プロパティを設定する)は機能しなくなりました。元の回答のリンクを見ると、これを壊すアップデートがJava 8にバックポートされています。リンクされたスレッドで述べたように、このソリューションは永久に機能するとは限りませんでした。これに基づいて、解決策は、受け入れられた回答で説明されている.getソリューションを備えたforkjoinpool.submitです。バックポートはこのソリューションの信頼性の欠如も修正すると思います。

ForkJoinPool fjpool = new ForkJoinPool(10);
System.out.println("stream.parallel");
IntStream range = IntStream.range(0, 20);
fjpool.submit(() -> range.parallel()
        .forEach((int theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();
System.out.println("list.parallelStream");
int [] array = IntStream.range(0, 20).toArray();
List<Integer> list = new ArrayList<>();
for (int theInt: array)
{
    list.add(theInt);
}
fjpool.submit(() -> list.parallelStream()
        .forEach((theInt) ->
        {
            try { Thread.sleep(100); } catch (Exception ignore) {}
            System.out.println(Thread.currentThread().getName() + " -- " + theInt);
        })).get();

ForkJoinPool.commonPool().getParallelism()デバッグモードで実行しても、並列処理の変更がわかりません。
d-coder

ありがとう。私はいくつかのテスト/研究を行い、答えを更新しました。以前のバージョンで機能するため、アップデートによって変更されたようです。
Tod Casasent

なぜこれがunreported exception InterruptedException; must be caught or declared to be thrown続くのですか:catchループ内のすべての例外があってもです。
ロッキー李

ロッキー、エラーは表示されません。Javaのバージョンと正確な行を知っていると役立ちます。「InterruptedException」は、スリープの周りのtry / catchがバージョンで適切に閉じられていないことを示しています。
Tod Casasent

13

次のプロパティを使用して、デフォルトの並列処理を変更できます。

-Djava.util.concurrent.ForkJoinPool.common.parallelism=16

より多くの並列処理を使用するように設定できます。


これはグローバル設定ですが、parallelStreamを増やすように機能します
meadlai

これは、openjdkバージョン "1.8.0_222"で私にとってはうまくいった
abbas

上記と同じ人が、これはOpenJDKの「11.0.6」に私のために働いていない
アッバス

8

使用されているスレッドの実際の数を測定するには、以下を確認できますThread.activeCount()

    Runnable r = () -> IntStream
            .range(-42, +42)
            .parallel()
            .map(i -> Thread.activeCount())
            .max()
            .ifPresent(System.out::println);

    ForkJoinPool.commonPool().submit(r).join();
    new ForkJoinPool(42).submit(r).join();

これにより、4コアCPUで次のような出力が生成されます。

5 // common pool
23 // custom pool

.parallel()それがなければ:

3 // common pool
4 // custom pool

6
Thread.activeCount()は、ストリームを処理しているスレッドを通知しません。代わりにThread.currentThread()。getName()にマップしてから、distinct()を続けます。次に、プール内のすべてのスレッドが使用されるわけではないことに気付くでしょう...処理に遅延を追加すると、プール内のすべてのスレッドが使用されます。
keyoxy 2016

7

これまでは、この質問の回答に記載されているソリューションを使用していました。さて、私はそのためのParallel Stream Supportという小さなライブラリを思いつきました。

ForkJoinPool pool = new ForkJoinPool(NR_OF_THREADS);
ParallelIntStreamSupport.range(1, 1_000_000, pool)
    .filter(PrimesPrint::isPrime)
    .collect(toList())

しかし、@ PabloMatiasGomezがコメントで指摘したように、共通プールのサイズに大きく依存する並列ストリームの分割メカニズムに関して欠点があります。HashSetからの並列ストリームが並列で実行されないを参照してください。

このソリューションを使用して、作業の種類ごとに個別のプールを用意していますが、使用しない場合でも、共通プールのサイズを1に設定できません。



1

次のようにカスタム ForkJoinPool を試し、プールサイズを調整しました。

private static Set<String> ThreadNameSet = new HashSet<>();
private static Callable<Long> getSum() {
    List<Long> aList = LongStream.rangeClosed(0, 10_000_000).boxed().collect(Collectors.toList());
    return () -> aList.parallelStream()
            .peek((i) -> {
                String threadName = Thread.currentThread().getName();
                ThreadNameSet.add(threadName);
            })
            .reduce(0L, Long::sum);
}

private static void testForkJoinPool() {
    final int parallelism = 10;

    ForkJoinPool forkJoinPool = null;
    Long result = 0L;
    try {
        forkJoinPool = new ForkJoinPool(parallelism);
        result = forkJoinPool.submit(getSum()).get(); //this makes it an overall blocking call

    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        if (forkJoinPool != null) {
            forkJoinPool.shutdown(); //always remember to shutdown the pool
        }
    }
    out.println(result);
    out.println(ThreadNameSet);
}

これは、プールがデフォルトよりも多くのスレッドを使用していることを示す出力です4

50000005000000
[ForkJoinPool-1-worker-8, ForkJoinPool-1-worker-9, ForkJoinPool-1-worker-6, ForkJoinPool-1-worker-11, ForkJoinPool-1-worker-10, ForkJoinPool-1-worker-1, ForkJoinPool-1-worker-15, ForkJoinPool-1-worker-13, ForkJoinPool-1-worker-4, ForkJoinPool-1-worker-2]

しかし、実際には奇妙なことがあり、ThreadPoolExecutor次のように使用して同じ結果を達成しようとしました:

BlockingDeque blockingDeque = new LinkedBlockingDeque(1000);
ThreadPoolExecutor fixedSizePool = new ThreadPoolExecutor(10, 20, 60, TimeUnit.SECONDS, blockingDeque, new MyThreadFactory("my-thread"));

でも失敗した。

それだけで起動しますparallelStreamを新しいスレッドで、その後、他のすべては、ちょうど同じで再びことを証明してparallelStream使用しますForkJoinPoolをその子スレッドを開始します。


他のエグゼキュターを許可しないことの背後にある考えられる理由は何でしょうか?
omjego

@omjegoこれは良い質問です。おそらく、新しい質問を開始し、アイデアを詳しく説明するための詳細を提供できます;)
Hearen

1

AbacusUtilを入手してください。スレッド番号は、並列ストリームに指定できます。これがサンプルコードです:

LongStream.range(4, 1_000_000).parallel(threadNum)...

情報開示:私はAbacusUtilの開発者です。


1

実装ハックに依存したくない場合は、組み合わせmapcollectセマンティクスを生成するカスタムコレクターを実装することで同じことを実現する方法が常にあります。ForkJoinPoolに限定されません。

list.stream()
  .collect(parallelToList(i -> fetchFromDb(i), executor))
  .join()

幸い、これはすでにここで行われていて、Maven Centralで入手できます。http: //github.com/pivovarit/parallel-collectors

免責事項:私はそれを書いて責任を負います。


0

サードパーティのライブラリを使用しても構わない場合は、cyclops-reactを使用して、同じパイプライン内でシーケンシャルストリームとパラレルストリームを混在さ、カスタムのForkJoinPoolsを提供できます。例えば

 ReactiveSeq.range(1, 1_000_000)
            .foldParallel(new ForkJoinPool(10),
                          s->s.filter(i->true)
                              .peek(i->System.out.println("Thread " + Thread.currentThread().getId()))
                              .max(Comparator.naturalOrder()));

または、シーケンシャルストリーム内で処理を続行したい場合

 ReactiveSeq.range(1, 1_000_000)
            .parallel(new ForkJoinPool(10),
                      s->s.filter(i->true)
                          .peek(i->System.out.println("Thread " + Thread.currentThread().getId())))
            .map(this::processSequentially)
            .forEach(System.out::println);

[開示私はc​​yclops-reactの主要開発者です]


0

カスタムThreadPoolは必要ないが、並行タスクの数を制限したい場合は、以下を使用できます。

List<Path> paths = List.of("/path/file1.csv", "/path/file2.csv", "/path/file3.csv").stream().map(e -> Paths.get(e)).collect(toList());
List<List<Path>> partitions = Lists.partition(paths, 4); // Guava method

partitions.forEach(group -> group.parallelStream().forEach(csvFilePath -> {
       // do your processing   
}));

(これを求める重複した質問はロックされているので、ここで私を負担してください)


-2

このForkJoinWorkerThreadFactoryを実装して、Fork-Joinクラスに注入できます。

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

これを行うには、Fork-Joinプールのこのコンストラクターを使用できます。

注:-1.これを使用する場合は、新しいスレッドの実装に基づいて、JVMからのスケジューリングが影響を受けることを考慮に入れてください。これは通常、フォーク結合スレッドを異なるコアにスケジュールします(計算スレッドとして扱われます)。2.スレッドへのフォーク結合によるタスクのスケジューリングは影響を受けません。3.並列ストリームがどのようにfork-joinからスレッドを選択しているか(実際に適切なドキュメントを見つけることができなかった)を理解していないため、別のthreadNamingファクトリーを使用して、並列ストリームのスレッドが選択されているかどうかを確認してください。あなたが提供するcustomThreadFactoryから。4. commonThreadPoolはこのcustomThreadFactoryを使用しません。


指定したものをどのように使用するかを示す使用可能な例を提供できますか?
J.マレー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.