Данная статья может быть интересна тем, кто только изучает Stream API, либо набирает практический опыт их использования. В ней раскрывается функционал, плюсы и минусы использования Parallel Stream, но не касаемся использования последовательных Stream API в целом.

Параллельные потоки стали мощной функцией в Java 8 и более поздних версиях, предлагая разработчикам возможность без особых усилий выполнять операции сбора данных параллельно. Используя возможности многопоточности современных компьютеров, параллельные потоки могут значительно повысить производительность вашего кода. В этой статье мы рассмотрим несколько примеров использования параллельных потоков, подчеркнув их преимущества в различных сценариях.

Для чего нужно параллелить потоки:

1. Обработка больших наборов данных

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

2. Операции с интенсивным использованием ЦП

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

3. Улучшенная производительность ввода/вывода

Параллельные потоки также могут улучшить операции ввода-вывода, такие как чтение или запись больших файлов, выполнение сетевых запросов или запросов к базе данных. Разделив операции ввода-вывода на более мелкие фрагменты и выполняя их параллельно, вы можете в полной мере использовать доступные системные ресурсы. Это может привести к сокращению времени выполнения, особенно при одновременном выполнении нескольких операций ввода-вывода.

4. Сложные преобразования данных

При работе со сложными преобразованиями данных параллельные потоки могут упростить код и улучшить его читаемость. Рассмотрите сценарии, в которых вам нужно применить несколько операций, таких как фильтрация, сопоставление и сокращение, для преобразования набора объектов. Параллельные потоки могут эффективно обрабатывать промежуточные шаги, что приводит к более чистому и лаконичному коду. Это особенно полезно в таких сценариях, как обработка файлов журналов, анализ больших документов XML/JSON или преобразование данных в заданиях пакетной обработки.

5. Сокращение потока

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

Примеры использования Parallel Stream

Сам код использования параллельных потоков не сложный и чуть отличается от последовательных Stream:

1) Распараллеливание операции List

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

numbers.parallelStream()
       .map(n -> n*2)
       .forEach(System.out::println);

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

2) Распараллеливание операции сложения

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int summ = numbers.parallelStream()
                .reduce(0, Integer::sum);

Параллельный поток используется для вычисления суммы всех чисел в списке. Операция reduce объединяет значения с помощью функции Integer::sum, начиная с начального значения 0.

В чем разница stream().parallel() & parallelStream()?

Методы stream().parallel() и parallelStream() в Java представляют два разных способа создания параллельного потока.

1)stream().parallel(): этот метод используется для преобразования последовательного потока в параллельный поток. Его можно вызвать для любого объекта последовательного потока, чтобы включить параллельную обработку этого потока. Например:

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> parallelStream = list.stream().parallel();

В этом случае метод stream() создает последовательный поток из списка, а затем метод parallel() вызывается для последовательного потока, чтобы преобразовать его в параллельный поток.

2) parallelStream(): этот метод вызывается непосредственно для объекта коллекции для создания параллельного потока. Он возвращает параллельный поток, позволяющий выполнять параллельную обработку элементов коллекции. Например:

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> parallelStream = list.parallelStream();

В этом случае метод parallelStream() вызывается непосредственно в списке для создания параллельного потока.

Оба метода достигают одного и того же результата создания параллельного потока, но основное различие заключается в способе их вызова. Метод stream().parallel() вызывается для последовательного объекта потока, тогда как метод parallelStream() вызывается непосредственно для объекта коллекции.

Parallel Stream & ForkJoinPool

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

Существующая связь между параллельными потоками и инфраструктурой ForkJoinPool заключается в базовой реализации параллельных потоков. Когда вы создаете параллельный поток, он использует ForkJoinPool по умолчанию, предоставленный Java, для параллельного выполнения операций потока. Это означает, что работа по разделению данных и их распределению по нескольким потокам выполняется ForkJoinPool.

ForkJoinPool управляет пулом рабочих потоков и планирует выполнение подзадач параллельного потока этими потоками. Он динамически регулирует количество потоков в зависимости от доступных ядер ЦП и рабочей нагрузки. Таким образом обеспечивается эффективное использование системных ресурсов и повышается общая производительность обработки параллельных потоков.

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

Вместо заключения

Введение ParallelStream в Java произвело революцию в том, как мы выполняем потоковые операции. Благодаря возможности распределять задачи между несколькими потоками ParallelStream предлагает значительный прирост производительности для операций, требующих больших вычислительных ресурсов. Кроме того, его бесшовная интеграция с Stream API устраняет необходимость в ручном управлении потоками, упрощая процесс разработки. Используя ParallelStream, разработчики могут легко извлечь выгоду из преимуществ параллельной обработки, открывая новые уровни эффективности и скорости в своих приложениях.

Однако стоит держать в памяти тот момент, что ParallelStream дает нам "условно бесплатную" многопоточность за счет ресурсов имеющегося ForkJoinPool, что порой может быть просто не оправдано с точки зрения использования.

Комментарии (4)


  1. sshikov
    02.07.2023 13:30
    +6

    произвело революцию в том, как мы выполняем потоковые операции.

    Ну вот если честно — нет, не произвело. Причем вы же выше написали, почему — потому что не все операции подходят, потому что то, потому что сё. Много ограничений. Недостаточно гибкое управление создаваемыми потоками. Сами стримы и лямбды — да, это была революция, это очень сильно повлияло на то, как люди стали писать программы на Java. А параллельные стримы — ну я вот вообще вряд ли смогу вспомнить, видел ли я их когда-либо у кого-то в коде. Пожалуй что и нет.


  1. syrtin
    02.07.2023 13:30
    +1

    Для меня Stream API — более удобный, компактный, функциональный, но не всегда эффективный сам по себе, как и Parallel Stream. Зачастую нужно профилировать.))
    Кстати, если кто-то не в курсе, то начиная с версии 2017.3 в IDEA встроен Stream Debugger во вкладке Trace Current Stream Chain:


  1. AstarothAst
    02.07.2023 13:30

    На небольших коллекциях такое распараллеливание только замедляет процесс. В докладах Шипилёва звучала цифра в 10_000 элементов коллекции — после нее распараллеливание стрима может дать положительный эффект. А может и не дать, надо бенчить.


  1. RussianAirBorn
    02.07.2023 13:30

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