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


398

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

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

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

Попробуйте следующий пример. Есть несколько задач с интенсивным использованием процессора, выполняемых в отдельных потоках. Задачи используют параллельные потоки. Первое задание не выполняется, поэтому каждый шаг занимает 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, но вы всегда можете создать свой собственный ForkJoinPool и отправлять ему запросы.
edharned

7
Подсказка: Java-чемпион Heinz Kabutz проверяет ту же проблему, но с еще худшим влиянием: блокировка потоков общего пула соединений форка. См. Javaspecialists.eu/archive/Issue223.html
Пети

Ответы:


395

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

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


20
Подробности о решении описаны здесь blog.krecan.net/2014/03/18/…
Лукас,

3
Но также указано, что потоки используют ForkJoinPoolили это детали реализации? Ссылка на документацию была бы хороша.
Николай

6
@Lukas Спасибо за фрагмент. Я добавлю, что ForkJoinPoolэкземпляр должен быть, shutdown()когда он больше не нужен, чтобы избежать утечки потока. (пример)
Джек

5
Обратите внимание, что в Java 8 есть ошибка, заключающаяся в том, что, хотя задачи выполняются в экземпляре пользовательского пула, они все еще связаны с общим пулом: размер вычислений остается пропорциональным общему пулу, а не пользовательскому пулу. Исправлено в Java 10: JDK-8190974
Terran

3
@terran Эта проблема также была исправлена ​​для Java 8 bugs.openjdk.java.net/browse/JDK-8224620
Cutberto Ocampo

192

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


30
commonPoolИмеет фактически один меньше availableProcessors, что приводит к полной параллельности равняться , availableProcessorsпотому вызывающими подсчеты нитей , как один.
Марко Топольник

2
отправить возврат ForkJoinTask. Для подражания parallel() get()необходимо:stream.parallel().forEach(soSomething)).get();
Григорий Кислин

5
Я не уверен, что ForkJoinPool.submit(() -> stream.forEach(...))мои потоки будут работать с данными данными ForkJoinPool. Я ожидаю, что все Stream-Action выполняется в ForJoinPool как ОДНО действие, но внутренне все еще использует стандартный / общий ForkJoinPool. Где вы видели, что ForkJoinPool.submit () будет делать то, что вы говорите, что делает?
Фредерик Лейтенбергер,

@FredericLeitenberger Вы, вероятно, хотели разместить свой комментарий под ответом Лукаса.
assylias

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. Как упоминалось в связанных потоках, это решение не гарантировало работать вечно. Исходя из этого, решение представляет собой 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();

Я не вижу изменения в параллелизме, когда я делаю ForkJoinPool.commonPool().getParallelism()в режиме отладки.
d-кодер

Спасибо. Я провел некоторое тестирование / исследование и обновил ответ. Похоже, обновление изменило его, так как оно работает в более старых версиях.
Тод Касасент

Почему я продолжаю получать это: unreported exception InterruptedException; must be caught or declared to be thrownдаже со всеми catchисключениями в цикле.
Рокки Ли

Рокки, я не вижу никаких ошибок. Знание версии Java и точной строки поможет. «InterruptedException» предполагает, что попытка / уловка вокруг сна не закрыта должным образом в вашей версии.
Тод Касасент

13

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

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

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


Хотя это глобальная настройка, она работает для увеличения параллельного
потока

У меня это работало на 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-ядерному процессору, например:

5 // common pool
23 // custom pool

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

3 // common pool
4 // custom pool

6
Thread.activeCount () не сообщает вам, какие потоки обрабатывают ваш поток. Вместо этого сопоставьте с Thread.currentThread (). GetName (), за которым следует отдельный (). Тогда вы поймете, что не каждый поток в пуле будет использоваться ... Добавьте задержку к вашей обработке, и все потоки в пуле будут использованы.
Keyoxy

7

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

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

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

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


4

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

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


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


В чем может быть причина, по которой не пускают других исполнителей?
omjego

@omjego Это хороший вопрос, возможно, вы могли бы начать новый вопрос и предоставить более подробную информацию для разработки ваших идей;)
Слушай

1

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

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

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


1

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

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

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

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


0

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

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


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


Можете ли вы предоставить полезный пример, который продемонстрировал бы, как использовать то, что вы указали?
Дж. Мюррей
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.