что такое parallel stream java
Parallelism
Parallel computing involves dividing a problem into subproblems, solving those problems simultaneously (in parallel, with each subproblem running in a separate thread), and then combining the results of the solutions to the subproblems. Java SE provides the fork/join framework, which enables you to more easily implement parallel computing in your applications. However, with this framework, you must specify how the problems are subdivided (partitioned). With aggregate operations, the Java runtime performs this partitioning and combining of solutions for you.
One difficulty in implementing parallelism in applications that use collections is that collections are not thread-safe, which means that multiple threads cannot manipulate a collection without introducing thread interference or memory consistency errors. The Collections Framework provides synchronization wrappers, which add automatic synchronization to an arbitrary collection, making it thread-safe. However, synchronization introduces thread contention. You want to avoid thread contention because it prevents threads from running in parallel. Aggregate operations and parallel streams enable you to implement parallelism with non-thread-safe collections provided that you do not modify the collection while you are operating on it.
Note that parallelism is not automatically faster than performing operations serially, although it can be if you have enough data and processor cores. While aggregate operations enable you to more easily implement parallelism, it is still your responsibility to determine if your application is suitable for parallelism.
This section covers the following topics:
Executing Streams in Parallel
You can execute streams in serial or in parallel. When a stream executes in parallel, the Java runtime partitions the stream into multiple substreams. Aggregate operations iterate over and process these substreams in parallel and then combine the results.
Concurrent Reduction
Consider again the following example (which is described in the section Reduction) that groups members by gender. This example invokes the collect operation, which reduces the collection roster into a Map :
The following is the parallel equivalent:
This is called a concurrent reduction. The Java runtime performs a concurrent reduction if all of the following are true for a particular pipeline that contains the collect operation:
Ordering
The order in which a pipeline processes the elements of a stream depends on whether the stream is executed in serial or in parallel, the source of the stream, and intermediate operations. For example, consider the following example that prints the elements of an instance of ArrayList with the forEach operation several times:
This example consists of five pipelines. It prints output similar to the following:
This example does the following:
Side Effects
Laziness
Interference
Stateful Lambda Expressions
Avoid using stateful lambda expressions as parameters in stream operations. A stateful lambda expression is one whose result depends on any state that might change during the execution of a pipeline. The following example adds elements from the List listOfIntegers to a new List instance with the map intermediate operation. It does this twice, first with a serial stream and then with a parallel stream:
Note: This example invokes the method synchronizedList so that the List parallelStorage is thread-safe. Remember that collections are not thread-safe. This means that multiple threads should not access a particular collection at the same time. Suppose that you do not invoke the method synchronizedList when creating parallelStorage :
The example behaves erratically because multiple threads access and modify parallelStorage without a mechanism like synchronization to schedule when a particular thread may access the List instance. Consequently, the example could print output similar to the following:
Stream API & ForkJoinPool
Продолжаем серию полезностей, которыми мы делимся с вами. Теперь уже вновь по Java.
Если вы уже знакомы со Stream API и использовали его, то знаете, что это удобный способ обработки данных. С помощью различных встроенных операций, таких как map, filter, sort и других можно преобразовать входящие данные и получить результат. До появления стримов разработчик был вынужден императивно описывать процесс обработки, то есть создавать цикл for по элементам, затем сравнивать, анализировать и сортировать при необходимости. Stream API позволяет декларативно описать, что требуется получить без необходимости описывать, как это делать. Чем-то это напоминает SQL при работе с базами данных.
Стримы сделали Java-код компактнее и читаемее. Еще одной идеей при создании Stream API было предоставить разработчику простой способ распараллеливания задач, чтобы можно было получить выигрыш в производительности на многоядерных машинах. При этом нужно было избежать сложности, присущей многопоточному программированию. И это удалось сделать, в Stream API есть методы BaseStream::parallel и Collection.parallelStream(), которые возвращают параллельный стрим.
То есть, если у нас был код:
то его легко распараллелить, если изменить один вызов
либо в общем случае для произвольного stream:
Как и за всяким простым API, за parallelStream() скрывается сложный механизм распараллеливания операций. И разработчику придется столкнуться с тем, что использование параллельного стрима может не улучшить производительность, а даже ухудшить её, поэтому важно понимать, что происходит за вызовом parallelStream(). Есть статья Doug Lea о том, в каких случаях использование параллельных стримов даст положительный эффект. Следует обратить внимание на следующие факторы:
F — операция, которая будет применяться к каждому элементу стрима. Она должна быть независимой — то есть не оказывает влияние на другие элементы, кроме текущего и не зависит от других элементов (stateless non-interfering function)
S — источник данных (коллекция) эффективно разделима (efficiently splittable). Например, ArrayList — это эффективно разделимый источник, легко вычислить индексы и интервалы, которые можно обрабатывать параллельно. Также эффективно обрабатывать HashMap. BlockingQueue, LinkedList и большинство IO-источников это плохие кандидаты для параллельной обработки.
Оценка преимущества параллельной обработки. На современных машинах имеет смысл распараллеливать задачи, время выполнения которых превышает 100 микросекунд.
Таким образом, прежде чем использовать этот инструмент, нужно понять, насколько ваша задача укладывается в описанные ограничения.
Экспериментируя с parallel() наткнулись ещё на один интересный момент, связанный с текущей реализацией. Parallel() пытается исполнять ваш код в несколько потоков и становится интересно, кто эти потоки создаёт и как ими управляет.
Попробуем запустить такой код:
Уже интересно, оказывается, по умолчанию parallel stream используют ForkJoinPool.commonPool. Этот пул создается статически, то есть при первом обращении к ForkJoinPool, он не реагирует на shutdown()/shutdownNow() и живет, пока не будет вызван System::exit. Если задачам не указывать конкретный пул, то они будут исполняться в рамках commonPool.
Попробуем выяснить, каков же размер commonPool и посмотрим в исходники jdk1.8.0_111. Для читаемости убраны некоторые вызовы, которые не относятся к parallelism.
Из того же класса константа:
Нас интересует parallelism, который отвечает за количество воркеров в пуле. По-умолчанию, размер пула равен Runtime.getRuntime().availableProcessors() — 1, то есть на 1 меньше, чем количество доступных ядер. Когда вы создаете кастомный FJPool, то можно установить желаемый уровень параллелизма через конструктор. А для commonPool можно задать уровень через параметры JVM:
Сверху свойство ограничено числом 32767 (0x7fff);
Это может быть полезно, если вы не хотите отдавать все ядра под задачи ForkJoinPool, возможно, ваше приложение в обычном режиме утилизирует 4 из 8 CPU, тогда имеет смысл отдать под FJ оставшиеся 4 ядра.
Появляется вопрос, почему количество воркеров на 1 меньше количества ядер. Ответ можно увидеть в документации к ForkJoinPool.java:
When external threads submit to the common pool, they can perform subtask processing (see externalHelpComplete and related methods) upon joins. This caller-helps policy makes it sensible to set common pool parallelism level to one (or more) less than the total number of available cores, or even zero for pure caller-runs
То есть, когда некий тред отправляет задачу в common pool, то пул может использовать вызывающий тред (caller-thread) в качестве воркера. Вот почему в выводе программы мы видели main! Разгадка найдена, ForkJoinPool пытается загрузить своими задачами и вызывающий тред. В коде выше это main, но если вызовем код из другого треда, то увидим, что это работает и для произвольного потока:
Теперь мы знаем немного больше об устройстве ForkJoinPool и parallel stream. Оказывается, что количество воркеров parallel stream ограничено и эти воркеры общего назначения, то есть могут быть использованы любыми другими задачами, которые запускаются на commonPool. Попробуем понять, чем это чревато для нас при разработке.
В коде происходит следующее: мы пытаемся полностью занять пул, отправив туда parallelism + 1 задачу (то есть 3 штуки в данном случае). После этого запускаем параллельную обработку стрима из первого примера. По логам видно, что parallel стрим исполняется в один поток, так как все ресурсы пула исчерпаны. Не зная о такой особенности будет сложно понять, если в вашей программе вырастет время обработки какого то запроса через BaseStream::parallel.
Что же делать, если вы хотите быть уверены, что ваш код действительно будет распараллелен? Есть решение, нужно запустить parallel() на кастомном пуле, для этого нам придётся немного модифицировать код из примера выше и запустить код обработки данных, как Runnable на кастомном FJPool:
Окей, теперь мы добились своей цели и уверены, что наши вычисления под контролем и никто не может повлиять на них со стороны.
Прежде чем применять любой, даже самый простой инструмент необходимо выяснить его особенности и ограничения. Для parallel stream таких особенностей много и необходимо учитывать, насколько ваша задача подходит для распараллеливания. Parallel stream хорошо работают, если операции независимы и не хранят состояние, источник данных может быть легко разделен на сегменты для параллельной обработки и задачу действительно имеет смысл выполнять параллельно. Помимо этого нужно учесть особенности реализации и убедиться, что для важных вычислений вы используете отдельный пул потоков, а не делите с общим пулом приложения.
Вопросы и предложения, как всегда приветствуются, т.к. это является частью нашего курса по Java и нам интересно мнение по материалу.
[Перевод] Когда использовать параллельные stream-ы
Рассмотрим использование S.parallelStream().operation(F) вместо S.stream().operation(F) при условии, что операции независимы друг от друга, и, либо затратны с точки зрения вычислений, либо применяются к большому числу элементов эффективно расщепляемой (splittable) структуры данных, либо и то и другое. Точнее:
Фреймворк потоковой обработки не будет (и не может) настаивать на чем-либо из перечисленного выше. Если вычисления зависимы между собой, то их параллельное выполнение не имеет смысла, либо вообще будет вредным и приведет к ошибкам. К другим, производным от приведенных выше инженерных ограничений (issues) и компромиссов (tradeoffs), критериям относятся:
Если перефразировать все вышесказанное, то использование parallel() в случае неоправданно малого объема вычислений может стоить около 100 микросекунд, а использование в противном случае должно сохранить, по крайней мере, само это время (или возможно часы для очень больших задач). Конкретная стоимость и польза будет различаться со временем и для различных платформ, и, также, в зависимости от контекста. Например, запуск небольших вычислений параллельно внутри последовательного цикла усиливает эффект подъемов и спадов (микротесты производительности, в которых проявляется подобное, могут не отражать реальную ситуацию).
Вопросы и ответы
Она могла бы попытаться, но слишком часто решение будет неверным. Поиски полностью автоматического многоядерного параллелизма не привели к универсальному решению за последние тридцать лет, и, поэтому, фреймворк использует более надежный подход, требующий от пользователя лишь выбор между да или нет. Данный выбор основывается на, постоянно встречающихся и в последовательном программировании, инженерных проблемах, которые вряд ли полностью исчезнут когда-либо. Например, вы можете столкнуться со стократным замедлением при поиске максимального значения в коллекции содержащей единственный элемент в сравнение с использованием этого значения напрямую (без коллекции). Иногда JVM может оптимизировать подобные случаи за вас. Но это редко происходит в последовательных случаях, и никогда в случае параллельного режима. С другой стороны, можно ожидать, что, по мере развития, инструменты будут помогать пользователям принимать более верные решения.
Эту идею можно распространить и на другие соображения о том когда и как использовать параллелизм.
На данный момент, использующие I/O генераторы Stream ов JDK (например, BufferedReader.lines() ), приспособлены, главным образом, для использования в последовательном режиме, обрабатывая элементы один за другим по мере поступления. Поддержка высокоэффективной массовой (bulk) обработки буферизированного I/O возможна, но, на данный момент, это требует разработки специальных генераторов Stream ов, Spliterator ов и Collector ов. Поддержка некоторых общих случаев может быть добавлена в будущих релизах JDK.
Машины, обычно, располагают фиксированным числом ядер, и не могут магическим образом создавать новые при выполнении параллельных операций. Однако, до тех пор пока критерии выбора параллельного режима явно говорят за, сомневаться не в чем. Ваши параллельные задачи будут конкурировать за ЦПУ с другими и вы заметите меньшее ускорение. В большинстве случаев это все равно более эффективно, чем другие альтернативы. Лежащий в основе механизм спроектирован так, что если доступные ядра отсутствуют, то вы заметите лишь небольшое замедление в сравнение с последовательным вариантом, за исключением случаев когда система настолько перегружена, что тратит все свое время на переключение контекста вместо выполнения какой-то реальной работы, или настроена в расчете на то, что вся обработка выполняется последовательно. Если у вас такая система, то, возможно, администратор уже отключил использование много- поточности/ядерности в настройках JVM. А если администратором системы являетесь вы сами, то есть смысл это сделать.
Да. По крайней мере в какой-то степени. Но стоит принимать во внимание, что stream-фреймворк учитывает ограничения источников и методов при выборе того как это делать. В общем, чем меньше ограничений, тем больший потенциал параллелизма. С другой стороны нет гарантий, что фреймворк выявит и применит все имеющиеся возможности для параллелизма. В некоторых случаях, если у вас есть время и компетенции, собственное решение может намного лучше использовать возможности параллелизма.
Если вы придерживаетесь данных советов, то, обычно, достаточное чтобы иметь смысл. Предсказуемость не является сильной стороной современного аппаратного обеспечения и систем, и поэтому универсального ответа нет. Локальность кеша, характеристики GC, JIT-компиляция, конфликты обращения к памяти, расположение данных, политики диспетчеризации ОС, и наличие гипервизора являются одними из факторов имеющих значительное влияние. Производительность последовательного режима, также, подвержена их влиянию, которое, при использовании параллелизма, часто усиливается: проблема вызывающая 10-ти процентную разницу в случае последовательного выполнения может привести к 10-ти кратной разнице при параллельной обработке.
Мы не хотим говорить вам что делать. Появление для программистов новых способов делать что-то неправильно может пугать. Ошибки в коде, архитектуре, и оценках конечно же будут случаться. Десятилетия назад некоторые люди предсказывали, что наличие параллелизма на уровне приложения приведет к большим бедам. Но это так и не сбылось.
Русские Блоги
Java8-17-Stream параллельная обработка данных и производительность
Каталог статей
Параллельная обработка данных и производительность
В предыдущих трех главах мы видели новый интерфейс Stream, который позволяет обрабатывать наборы данных декларативным образом. Мы также объяснили, что изменение внешней итерации на внутреннюю позволяет встроенной библиотеке Java управлять обработкой элементов потока. Такой подход позволяет Java-программистам ускорить обработку наборов данных без явной оптимизации. Безусловно, наиболее важным преимуществом является возможность выполнять конвейер операций с этими коллекциями, который может автоматически использовать несколько ядер на компьютере.
Например, до Java 7 параллельная обработка наборов данных была очень сложной задачей. Во-первых, вы должны четко разделить структуру данных, содержащую данные, на части. Во-вторых, вам нужно назначить отдельный поток для каждого подраздела. В-третьих, вам необходимо синхронизировать их в нужное время, чтобы избежать нежелательных состояний гонки, дождаться завершения всех потоков и, наконец, объединить эти частичные результаты.
В Java 7 представлена структура, называемая ветвлением / слиянием, чтобы сделать эти операции более стабильными и менее подверженными ошибкам.
В этой главе мы поймем, как интерфейс Stream позволяет выполнять параллельные операции с наборами данных без особых усилий. Он позволяет декларативно превращать последовательные потоки в параллельные потоки. Вдобавок вы увидите, как Java вызывает в воображении, или, если точнее, то, как потоки используют структуру ветвления / слияния, введенную Java 7 за кулисами. Вы также обнаружите, что важно понимать, как параллельные потоки работают внутри, потому что, если вы проигнорируете этот аспект, вы можете получить неожиданные (и, возможно, неправильные) результаты из-за неправильного использования.
Мы специально продемонстрируем, что способ разделения параллельного потока на блоки данных перед параллельной обработкой блоков данных как раз и является источником этих ошибочных и необъяснимых результатов в некоторых случаях.
Поэтому мы узнаем, как контролировать этот процесс разделения, реализовав и используя ваш собственный Spliterator.
Параллельный поток
В примечаниях к главе 4 мы вкратце узнали, что интерфейс Stream позволяет очень удобно обрабатывать его элементы: вы можете вызвать источник коллекции parallelStream Метод преобразования коллекции в параллельный поток.
пример
Давайте попробуем эту идею на простом примере.
В более традиционных терминах Java этот код эквивалентен следующей итерации:
Кажется, это хорошая возможность воспользоваться преимуществами параллельной обработки, особенно когда n велико.
Как начать? Вы хотите синхронизировать переменную результата? Сколько потоков используется? Кто отвечает за создание номеров? Кто будет делать дополнение?
Совершенно не о чем беспокоиться. С параллельными потоками проблема намного проще!
Преобразование последовательного потока в параллельный поток
Мы можем преобразовать поток в параллельный поток, чтобы предыдущий процесс сокращения функции (то есть суммирование) выполнялся параллельно, вызывая параллельный метод в последовательном потоке:
Процесс выполнения параллельного потока:
Обратите внимание, что в действительности вызов параллельного метода в потоке последовательности не подразумевает каких-либо фактических изменений в самом потоке.
Фактически он устанавливает внутренний логический флаг, указывая, что вы хотите, чтобы все операции, выполняемые после вызова parallel, выполнялись параллельно.
Точно так же вам нужно только вызвать последовательный метод в параллельном потоке, чтобы превратить его в последовательный поток.
Например, вы можете:
Но последний параллельный или последовательный вызов повлияет на весь конвейер.
В этом случае конвейер будет выполняться параллельно, потому что это последний вызов.
Возвращаясь к нашему упражнению с числовым суммированием, мы сказали, что при запуске параллельной версии на многоядерном процессоре наблюдается значительное улучшение производительности.
Теперь у вас есть три способа выполнить одну и ту же операцию тремя разными способами (итеративная, последовательная индукция и параллельная индукция), давайте посмотрим, кто из них быстрее всех!
Измерение производительности потока
Мы утверждаем, что параллельный метод суммирования должен работать лучше, чем последовательный и итерационный методы. Однако в разработке программного обеспечения угадывание определенно не лучший способ!
Вы всегда должны следовать трем золотым правилам, особенно при оптимизации производительности: измерять, измерять и измерять.
Измеряет производительность функции, которая суммирует первые n натуральных чисел.
Этот метод принимает функцию и параметры типа long as. Он применяет функцию 10 раз к long, переданному методу, записывает время (в миллисекундах) каждого выполнения и возвращает самое короткое время выполнения.
Предположим, вы поместили все ранее разработанные методы в класс под названием ParallelStreams, вы можете использовать эту структуру, чтобы проверить, сколько времени требуется функции последовательного сумматора для суммирования первых десяти миллионов натуральных чисел:
Обратите внимание, что у нас должны быть оговорки по поводу этого результата. На время выполнения влияет множество факторов, например, сколько ядер поддерживает ваш компьютер.
Вы можете запустить эти коды на своей машине. Запустив его на ноутбуке i5 6200U, результат будет примерно таким:
Итеративная версия традиционного цикла for должна выполняться намного быстрее, потому что она более низкоуровневая и, что более важно, не требует упаковки или распаковки примитивных типов.
Если вы попытаетесь измерить его производительность:
Теперь протестируем параллельную версию функции:
Смотрите, что происходит:
Потоковый параллелизм не так хорош, как ожидалось
Это очень обидно, ведь параллельная версия метода суммирования намного медленнее последовательной.
Как вы объясните этот неожиданный результат?
На самом деле здесь есть две проблемы:
Iterate генерирует упакованные объекты, которые необходимо распаковать в числа для суммирования.
Нам сложно разделить итерацию на несколько независимых блоков для параллельного выполнения.
Второй вопрос немного интереснее, потому что вы должны понимать, что некоторые потоковые операции легче распараллелить, чем другие.
В частности, итерацию трудно разделить на небольшие части, которые можно выполнять независимо, поскольку каждое применение этой функции зависит от результата предыдущего приложения.
Это означает, что в данном конкретном случае процесс индукции не продолжается, как показано на рисунке выше; весь список чисел не готов в начале процесса индукции, поэтому поток не может быть эффективно разделен на небольшие блоки для параллельной обработки. Помечая поток как параллельный, вы фактически добавляете накладные расходы на последовательную обработку, а также разделяете каждую операцию суммирования на другой поток.
Это показывает, что параллельное программирование может быть сложным и иногда противоречивым.
Если он используется неправильно (например, операция, которую нелегко распараллелить, например, итерация), это может даже ухудшить общую производительность программы, поэтому при вызове этой, казалось бы, волшебной параллельной операции очень важно понимать, что произошло за ней. Нужно.
Используйте более адресный подход
Так как же использовать многоядерные процессоры для эффективного параллельного суммирования с потоками?
Мы обсуждали метод LongStream.rangeClosed в главе 5.
Этот метод имеет два преимущества перед итерацией.
LongStream.rangeClosed напрямую производит примитивные длинные числа без накладных расходов на упаковку и распаковку.
LongStream.rangeClosed сгенерирует диапазон чисел, который легко разделить на отдельные части.
Например, диапазон 120 можно разделить на 15、610、1115 и 16-20.
Давайте посмотрим на его производительность, когда он используется для потока последовательности, и посмотрим, важны ли накладные расходы на распаковку:
На этот раз результат:
Этот числовой поток намного быстрее, чем предыдущая версия с последовательным выполнением, в которой для генерации чисел использовался фабричный метод итерации, поскольку числовой поток позволяет избежать ненужных операций автоматической упаковки и распаковки для нецелевых потоков.
Видно, что выбор подходящей структуры данных часто более важен, чем распараллеливание алгоритма.
Но что, если к этой новой версии применяется параллельная потоковая передача?
Теперь передайте эту функцию методу тестирования:
ps: стократное улучшение производительности.
удивительный! Наконец, мы получаем параллельную индукцию, которая выполняется быстрее, чем последовательное выполнение, потому что на этот раз операция индукции может выполняться как граф выполнения параллельного потока. Это также показывает, что использование правильной структуры данных и последующая параллельная работа может обеспечить наилучшую производительность.
Тем не менее помните, что распараллеливание не обходится без затрат.
Сам процесс распараллеливания должен рекурсивно разделить поток, назначить операции индукции каждого подпотока разным потокам, а затем объединить результаты этих операций в одно значение. Но стоимость перемещения данных между несколькими ядрами может быть больше, чем вы думаете, поэтому важно убедиться, что время на параллельное выполнение работы в ядрах больше, чем время на передачу данных между ядрами.
В общем, распараллеливание во многих случаях невозможно или неудобно. Однако, прежде чем использовать параллельный поток для ускорения вашего кода, вы должны убедиться, что используете его правильно; если результат неверен, нет смысла быстро его вычислять.
Давайте рассмотрим распространенную ошибку.
Правильно используйте параллельные потоки
Основная причина ошибок, вызванных неправильным использованием параллельных потоков, заключается в том, что используемый алгоритм изменяет некоторое общее состояние.
Вот еще один способ сложить первые n натуральных чисел, но это изменит общий аккумулятор:
Такой код очень распространен, особенно для программистов, знакомых с парадигмой императивного программирования. Этот код очень похож на императивный способ, которым вы привыкли перебирать списки чисел: инициализировать аккумулятор, проходить элементы в списке один за другим и добавлять их в аккумулятор.
Что не так с таким кодом? К сожалению, это действительно безнадежно, потому что оно носит последовательный характер.
Каждый раз, когда вы посещаете total, будет конкуренция данных. Если вы попытаетесь исправить это с помощью синхронизации, вы потеряете смысл параллелизма.
Чтобы проиллюстрировать это, давайте попробуем сделать Stream параллельным:
Выполните тестовый метод и распечатайте результаты каждого выполнения:
Вы можете получить следующий результат:
На этот раз производительность метода не имеет значения, важно только то, что каждое выполнение будет возвращать другой результат, что далеко от правильного значения 50000005000000.
Корень проблемы в том, что метод, вызываемый в forEach, имеет побочные эффекты, которые изменяют изменяемое состояние объектов, совместно используемых несколькими потоками.
Если вы хотите использовать параллельный поток и не хотите вызывать подобные аварии, вы должны избегать этой ситуации.
Теперь вы знаете, что общее изменяемое состояние влияет на параллельные потоки и параллельные вычисления.
сейчас,Не забывайте избегать совместного использования изменяемого состояния и убедитесь, что параллельные потоки получают правильные результаты.
Далее мы увидим несколько практических предложений, вы сможете судить, когда можно использовать параллельные потоки для повышения производительности.
Эффективно используйте параллельные потоки
Вообще говоря, невозможно и бессмысленно давать какие-либо количественные советы о том, когда использовать параллельные потоки, потому что что-то вроде «только если есть хотя бы одна тысяча (или один миллион или любое другое число) ) Используйте только параллельные потоки для элементов) »предложение может быть правильным для определенной операции на определенной машине, но в другом случае, с небольшой разницей, оно может быть совершенно неверным. Тем не менее, мы можем, по крайней мере, высказать некоторые качественные мнения, которые помогут вам решить, нужно ли использовать параллельные потоки в конкретной ситуации.
Обратите внимание на упаковку. Операции автоматической упаковки и распаковки значительно снизят производительность.
В Java 8 есть примитивные типы потоков (IntStream, LongStream, DoubleStream), чтобы избежать такого рода операций, но эти потоки следует использовать везде, где это возможно.
Некоторые операции с параллельными потоками выполняются хуже, чем с последовательными.
В частности, такие операции, как limit и findFirst, которые зависят от порядка элементов, очень дороги для выполнения в параллельных потоках.
Например, findAny будет работать лучше, чем findFirst, потому что его не нужно выполнять последовательно. Вы всегда можете вызвать неупорядоченный метод, чтобы превратить упорядоченный поток в неупорядоченный поток. Затем, если вам нужно n элементов в потоке вместо, в частности, первого n, ограничение на вызов для неупорядоченного параллельного потока может быть более эффективным, чем для одного упорядоченного потока (например, источником данных является список).
Для меньших объемов данных выбор параллельных потоков почти никогда не бывает хорошим решением.
Преимущества параллельной обработки нескольких элементов не оправдывают дополнительных накладных расходов, вызванных распараллеливанием.
Подумайте, легко ли разложить структуру данных за потоком.
Например, эффективность разделения ArrayList намного выше, чем у LinkedList, потому что первый может быть разделен поровну без обхода, а второй должен быть пройден.
Кроме того, поток примитивного типа, созданный с помощью метода фабрики диапазонов, также можно быстро разложить.
Характеристики самого потока, а также способ, которым промежуточные операции в конвейере изменяют поток, могут изменять производительность процесса декомпозиции. Например, поток SIZED может быть разделен на две части равного размера, так что каждая часть может обрабатываться параллельно более эффективно, но количество элементов, которые могут быть отброшены операцией фильтрации, невозможно предсказать, что приводит к неизвестному размеру самого потока.
Также учтите, велика или мала стоимость шага слияния в операции терминала (например, метод объединения в Collector). Если этот этап является дорогостоящим, тогда стоимость объединения частичных результатов каждого подпотока может превысить улучшение производительности, полученное с помощью параллельных потоков.
Принцип реализации, лежащий в основе
Пример параллельной сводки доказывает, что для правильного использования параллельного потока важно понимать его внутренние принципы, поэтому мы внимательно изучим структуру ветвления / слияния в следующем разделе.