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

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

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

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

попробуйте следующий пример. Есть некоторые интенсивные задачи 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);
    }
}

10 ответов


на самом деле есть трюк, Как выполнить параллельную операцию в определенном пуле 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.вилка который указывает: "организует асинхронное выполнение этой задачи в пуле, в котором выполняется текущая задача, если это применимо, или с помощью ForkJoinPool.commonPool () если не inForkJoinPool ()"


параллельные потоки используют значение по умолчанию 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 потоков.

пример последнего на моей машине, которая имеет 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 элементов в потоке были обработаны одновременно.


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

использование ForkJoinPool и submit для параллельного потока не надежно использует все потоки. Если вы посмотрите на это ( параллельный поток из HashSet не работает параллельно ) и этот ( Почему параллельный поток не использует все потоки ForkJoinPool? ), вы увидите рассуждения.

краткая версия: если ForkJoinPool / submit не работает для вас, используйте

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "10");

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

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

но, как отметил @PabloMatiasGomez в комментариях, есть недостатки в отношении механизма разделения параллельных потоков, который сильно зависит от размера общего пула. См.параллельный поток из HashSet не работает параллельно .

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


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

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

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

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


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

[раскрытие я ведущий разработчик cyclops-react]


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


Примечание: В JDK 10 реализовано исправление, которое гарантирует, что пользовательский пул потоков использует ожидаемое количество потоков.

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