Пользовательский пул потоков в параллельном потоке Java 8

Можно ли указать собственный пул потоков для Java 8 параллельный поток? Я не могу найти его нигде.

Представьте, что у меня есть серверное приложение, и я хотел бы использовать параллельные потоки. Но приложение является большим и многопоточным, поэтому я хочу его разделить. Я не хочу выполнять медленную работу в одном модуле задач прикладного блока из другого модуля.

Если я не могу использовать разные пулы потоков для разных модулей, это означает, что я не могу безопасно использовать параллельные потоки в большинстве реальных ситуаций.

Попробуйте следующий пример. В отдельных потоках выполняются некоторые интенсивные задачи ЦП. Задачи используют параллельные потоки. Первая задача разбита, поэтому каждый шаг занимает 1 секунду (имитируется потоком сна). Проблема в том, что другие потоки застревают и ждут завершения сломанной задачи. Это надуманный пример, но представьте, что приложение сервлета и кто-то отправляет многолетнюю задачу в общий пул соединений fork.

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);
    }
}

Ответ 1

На самом деле есть трюк, как выполнить параллельную операцию в отдельном пуле fork-join. Если вы выполняете его как задачу в пуле fork-join, он остается там и не использует общий.

ForkJoinPool forkJoinPool = new ForkJoinPool(2);
forkJoinPool.submit(() ->
    //parallel task here, for example
    IntStream.range(1, 1_000_000).parallel().filter(PrimesPrint::isPrime).collect(toList())
).get();

Трюк основан на ForkJoinTask.fork, который указывает: "Устанавливает асинхронное выполнение этой задачи в пуле, в котором запущена текущая задача, если применимо, или с помощью ForkJoinPool.commonPool(), если не inForkJoinPool()"

Ответ 2

Параллельные потоки используют по умолчанию ForkJoinPool.commonPool, который по умолчанию имеет на один поток меньше, поскольку у вас есть процессоры, как возвращает 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) + " ");
});

Вывод:

215 216 216 216 216 216 216 216 315 316 316 316 316 316 316 316 415 416 416 416

Таким образом, вы можете видеть, что параллельный поток обрабатывает 8 элементов одновременно, то есть использует 8 потоков. Однако, если я раскомментирую закомментированную строку, получится:

215 215 215 215 215 216 216 216 216 216 216 216 216 216 216 216 216 216 216 216

На этот раз параллельный поток использовал 20 потоков, и все 20 элементов в потоке были обработаны одновременно.

Ответ 3

В качестве альтернативы трюку запуска параллельного вычисления внутри вашего собственного 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
);

Ответ 4

Исходное решение (установка свойства общего параллелизма ForkJoinPool) больше не работает. Глядя на ссылки в исходном ответе, обновление, которое нарушает это, было обратно перенесено на Java 8. Как упоминалось в связанных потоках, это решение не гарантировало работать вечно. Исходя из этого, решение представляет собой forkjoinpool.submit с решением .get, обсуждаемым в принятом ответе. Я думаю, что backport также исправляет ненадежность этого решения.

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();

Ответ 5

Мы можем изменить параллелизм по умолчанию, используя следующее свойство:

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

который может быть настроен на использование большего параллелизма.

Ответ 6

Чтобы измерить фактическое количество используемых потоков, вы можете проверить 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-ядерному процессору, например:

5 // common pool
23 // custom pool

Без .parallel() он дает:

3 // common pool
4 // custom pool

Ответ 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, даже если я его не использую.

Ответ 8

Примечание. Похоже, в JDK 10 реализовано исправление, обеспечивающее использование ожидаемого количества потоков в пуле пользовательских потоков.

Параллельное выполнение потока в пользовательском ForkJoinPool должно подчиняться параллелизму https://bugs.openjdk.java.net/browse/JDK-8190974

Ответ 9

Иди, чтобы получить AbacusUtil. Номер потока может быть указан для параллельного потока. Вот пример кода:

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

Раскрытие информации: я разработчик AbacusUtil.

Ответ 10

Я попробовал пользовательский 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 поток будет использовать ForkJoinPool для запуска своих дочерних потоков.

Ответ 11

Если вы не хотите полагаться на хаки реализации, всегда есть способ добиться того же самого путем реализации пользовательских сборщиков, которые будут объединять семантику map и collect... и вы не будете ограничены ForkJoinPool:

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

К счастью, это уже сделано и доступно на Maven Central: http://github.com/pivovarit/parallel-collectors

Отказ от ответственности: я написал это и беру на себя ответственность за это.

Ответ 12

Если вы не возражаете против использования сторонней библиотеки, 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);

[Раскрытие информации Я ведущий разработчик реакции циклопов]

Ответ 13

Если вам не нужен пользовательский 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   
}));

(Дубликат вопроса об этом заблокирован, поэтому, пожалуйста, несите меня сюда)

Ответ 14

Вы можете попробовать реализовать этот 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. Планирование задач с помощью fork-join к потокам не пострадает.  3. Не совсем понял, как параллельный поток выбирает потоки  из fork-join (не смог найти нужную документацию по нему), поэтому попробуйте  использование другой фабрики ThreadNaming, чтобы убедиться, что потоки  в параллельном потоке выбираются из customThreadFactory, что вы  предоставлять.  4. commonThreadPool не будет использовать этот customThreadFactory.