Параллельные стримы
Один pipeline можно выполнить параллельно
Обычный stream() выполняется последовательно.
parallelStream() у коллекции или вызов parallel()
переводит pipeline в параллельный режим: обработка элементов может быть разделена на части
и выполняться одновременно.
long count = List.of(10, 20, 30, 40, 50).parallelStream()
.filter(value -> value >= 30)
.count();
Смысл результата тот же, что у последовательного stream. Разница в способе выполнения, а не в API операций.
Параллельно не значит быстрее
Разделение работы, координация частей и объединение результата сами требуют времени. На небольшой коллекции эта цена легко окажется выше пользы от параллельности. Выигрыш зависит от размера данных, стоимости операции над элементом, возможности хорошо разделить источник и доступных вычислительных ресурсов.
Побочные эффекты становятся опаснее
Lambda внутри параллельного pipeline может выполняться одновременно для разных элементов. Если несколько вызовов меняют одно общее изменяемое состояние, появляются гонки и непредсказуемые результаты.
List<Integer> result = IntStream.range(0, 1_000)
.parallel()
.filter(value -> value % 2 == 0)
.boxed()
.toList();
Здесь каждый шаг описывает преобразование данных и не изменяет общий внешний список.
Такой стиль намного безопаснее, чем добавлять элементы в одну изменяемую коллекцию из forEach.
Порядок выполнения и порядок результата — разные вещи
Даже если у stream есть encounter order, отдельные элементы могут обрабатываться в разное время.
forEach у параллельного stream не обязан выполнять действие в исходном порядке.
Если порядок побочного эффекта действительно важен, существует forEachOrdered,
но сохранение порядка может уменьшить пользу от параллельности.
Операция объединения должна быть корректной для параллельности
При reduce части stream могут сворачиваться отдельно,
а затем объединяться. Поэтому операция объединения должна быть ассоциативной:
группировка вычислений не должна менять результат.
Сложение подходит, вычитание — нет.
int sum = IntStream.rangeClosed(1, 100)
.parallel()
.reduce(0, Integer::sum);
Параллельный stream полезен прежде всего для достаточно крупной вычислительной работы, которую можно независимо выполнять над частями данных. Для блокирующих операций, маленьких коллекций и кода с общим изменяемым состоянием последовательный вариант часто остаётся понятнее и предсказуемее.