Для чего нужны параллельные стримы
Должен ли я всегда использовать параллельный поток, когда это возможно?
До тех пор, пока меня не волнует порядок, всегда ли было бы полезно использовать параллель? Можно было бы подумать, что это быстрее делит работу на большее количество ядер.
Есть ли другие соображения? Когда следует использовать параллельный поток и когда следует использовать непараллельный?
ОТВЕТЫ
Ответ 1
Параллельный поток имеет гораздо более высокие накладные расходы по сравнению с последовательным. Координация потоков занимает значительное количество времени. Я бы использовал последовательные потоки по умолчанию и рассматривал только параллельные, если
У меня есть огромное количество элементов для обработки (или обработка каждого элемента требует времени и параллелизуема)
У меня проблема с производительностью в первую очередь
Я еще не запускаю этот процесс в многопоточной среде (например: в веб-контейнере, если у меня уже есть много запросов для параллельной обработки, добавление дополнительного слоя parallelism внутри каждого запрос может иметь более отрицательные, чем положительные эффекты)
В любом случае, мера, не угадайте! Только измерение скажет вам, стоит ли parallelism или нет.
Ответ 2
Stream API был разработан, чтобы упростить запись вычислений таким образом, чтобы они были отвлечены от того, как они будут выполняться, обеспечивая переход между последовательными и параллельными.
Во-первых, обратите внимание, что параллелизм не дает никаких преимуществ, кроме возможности более быстрого выполнения, когда доступно больше ядер. Параллельное выполнение всегда будет включать в себя больше работы, чем последовательный, поскольку в дополнение к решению проблемы он также должен выполнять диспетчеризацию и координирование подзадач. Надеемся, что вы сможете быстрее получить ответ, разбив работу на нескольких процессорах; происходит ли это на самом деле, зависит от множества вещей, включая размер вашего набора данных, сколько вычислений вы делаете для каждого элемента, характер вычисления (в частности, обрабатывает ли один элемент взаимодействие с обработкой других?), количество доступных процессоров и количество других задач, конкурирующих за эти процессоры.
Кроме того, обратите внимание, что параллелизм также часто демонстрирует недетерминизм в вычислении, который часто скрывается последовательными реализациями; иногда это не имеет значения или может быть смягчено путем ограничения задействованных операций (т.е. операторы сокращения должны быть апатридами и ассоциативными).
Но реальность довольно сложная. Поэтому, пока вы не достигнете опыта, сначала определите, когда последовательная обработка фактически стоит вам что-то, а затем измерьте, поможет ли параллелизм.
Ответ 3
Я смотрел один из presentations Brian Goetz (Java Language Architect и руководство по спецификации для Lambda Expressions). Он подробно объясняет следующие 4 момента, которые следует рассмотреть перед переходом к распараллеливанию:
Расходы на расщепление/разложение
— Иногда расщепление дороже, чем просто работа! Расходы на командировку/управление
— Может делать много работы за время, которое требуется, чтобы передать работу другому потоку.
Стоимость комбинации результатов
— Иногда комбинация включает в себя копирование большого количества данных. Например, добавление чисел дешево, тогда как слияние наборов дорого.
Местность
— Слон в комнате. Это важный момент, который каждый может пропустить. Вы должны учитывать промахи в кэше, если ЦП ожидает данных из-за промахов в кеше, тогда вы ничего не выиграете при распараллеливании. Именно поэтому массивные источники распараллеливают лучшее, поскольку следующие индексы (рядом с текущим индексом) кэшируются, и меньше шансов на то, что процессор будет испытывать промаху в кеше.
Он также упоминает относительно простую формулу, чтобы определить вероятность параллельного ускорения.
Модель NQ:
где,
N = количество элементов данных
Q = количество работы за элемент
Ответ 4
Ответ 5
Другие ответы уже охватили профилирование, чтобы избежать преждевременной оптимизации и накладных расходов при параллельной обработке. Этот ответ объясняет идеальный выбор структур данных для параллельной потоковой передачи.
Другим важным фактором, который объединяет все эти структуры данных, является то, что они обеспечивают отличное расположение ссылок при последовательной обработке: последовательные ссылки на элементы хранятся вместе в памяти. Объекты, на которые ссылаются эти ссылки, могут не находиться близко друг к другу в памяти, что уменьшает локальность ссылок. Ссылочное местоположение оказывается критически важным для распараллеливания массовых операций: без него потоки тратят большую часть своего времени простоя, ожидая передачи данных из памяти в кэш процессоров. Структуры данных с наилучшим местоположением ссылок являются примитивными массивами, потому что сами данные хранятся непрерывно в памяти.
Ответ 6
Никогда не распараллеливайте бесконечный поток с пределом. Вот что происходит:
Точно так же, не используйте параллельный, если поток упорядочен и имеет намного больше элементов, чем вы хотите обработать, например
Это может работать намного дольше, потому что параллельные потоки могут работать на множестве диапазонов номеров вместо критического 0-100, в результате чего это займет очень много времени.
Ответ 7
Java 8 Stream API: шпаргалка для программиста
Обработка данных — стандартная задача при разработке. Раньше для этого приходилось использовать циклы или рекурсивные функции. С появлением в Java 8 Stream API процесс обработки данных значительно ускорился. Этот инструмент языка позволяет описать, как нужно обработать данные, кратко и емко.
Что такое Java Stream API
Это новый инструмент языка Java, который позволяет использовать функциональный стиль при работе с разными структурами данных.
Для начала стриму нужен источник, из которого он будет получать объекты. Чаще всего это коллекции, но не всегда. Например, можно взять в качестве источника генератор, у которого заданы правила создания объектов.
Данные в стриме обрабатываются на промежуточных операциях. Например: мы можем отфильтровать данные, пропустить несколько элементов, ограничить выборку, выполнить сортировку. Затем выполняется терминальная операция. Она поглощает данные и выдает результат.
Stream на примере простой задачи
Для наглядности посмотрим на примере использование стримов в сравнении со старым решением аналогичной задачи.
Задача — найти сумму нечетных чисел в коллекции.
Решение с методами стрима:
Здесь мы видим функциональный стиль. Без стримов эту же задачу приходится решать через использование цикла:
Да, на первый взгляд цикл выглядит более понятным. Но это вопрос опыта взаимодействия со стримами. Очень быстро привыкаешь к тому, что можно обрабатывать данные без использования циклов.
Преимущества Stream
Благодаря стримам больше не нужно писать стереотипный код каждый раз, когда приходится что-то делать с данными: сортировать, фильтровать, преобразовывать. Разработчики меньше думают о стандартной реализации и больше времени уделяют более сложным вещам.
Еще несколько преимуществ стримов:
Даже сложные операции по обработке данных благодаря Stream API выглядят лаконично и понятно. В общем, писать становится удобнее, а читать — проще.
Как создавать стримы
В таблице ниже — основные способы создания стримов.
Источник | Способ | Пример |
Коллекция | collection.stream() | Collection collection = Arrays.asList(«f5», «b6», «z7»); Stream collectionS = collection.stream(); |
Значения | Stream.of(v1,… vN) | Stream valuesS = Stream.of(«f5», «b6», «z7»); |
Примитивы | IntStream.of(1, … N) | IntStream intS = IntStream.of(9, 8, 7); |
DoubleStream.of(1.1, … N) | DoubleStream doubleS = DoubleStream.of(2.4, 8.9); | |
Массив | Arrays.stream(arr) | String[] arr = <"f5","b6","z7">; Stream arrS = Arrays.stream(arr); |
Файл — каждая новая строка становится элементом | Files.lines(file_path) | Stream fromFileS = Files.lines(Paths.get(«doc.txt»)) |
Stream.builder | Stream.builder().add(. ). build() | Stream.builder().add(«f5»).add(«b6»).build() |
Стримы можно создавать не только из файлов, но и из списка объектов какой-либо директории или файлов, находящихся в какой-либо части дерева файловой системы.
В Stream.iterate мы задаем начальное значение, а также указываем, как будем получать следующее, используя предыдущий результат:
Stream.generate позволяет бесконечно генерировать постоянные и случайные значения, которые соответствуют указанному выражению.
Если хотите узнать больше об этих и других способах, читайте документацию Stream.
Методы стримов
В Java 8 Stream API доступны методы двух видов — конвейерные и терминальные. Кроме них можно выделить ряд спецметодов для работы с числовыми стримами и несколько методов для проверки параллельности/последовательности. Но это формальное разделение.
Конвейерных методов в стриме может быть много. Терминальный метод — только один. После его выполнения стрим завершается.
Пока вы не вызвали терминальный метод, ничего не происходит. Все потому, что конвейерные методы ленятся. Это значит, что они обрабатывают данные и ждут команды, чтобы передать их терминальному методу.
Конвейерные
Терминальные
Вот несколько интересных примеров:
Методы числовых стримов
Это специальные методы, которые работают только со стримами с числовыми примитивами.
Еще несколько методов
Напоследок посмотрим еще несколько полезных методов, которые помогают управлять последовательными и параллельными стримами — как минимум быстро их определять.
Метод | Что сделает | Использование |
isParallel | скажет, параллельный стрим или нет | someStream.isParallel() |
parallel | сделает стрим параллельным или вернет сам себя | someStream = stream.parallel() |
sequential | сделает стрим последовательным или вернет сам себя | someStream = stream.sequential() |
Не рекомендуется применять параллельность для выполнения долгих операций (например, извлечения данных из базы), потому что все стримы работают с общим пулом. Долгие операции могут остановить работу всех параллельных стримов в Java Virtual Machine из-за того, что в пуле не останется доступных потоков.
Чтобы избежать такой проблемы, используйте параллельные стримы только для коротких операций, выполнение которых занимает миллисекунды, а не секунды и тем более минуты.
В Stream API по умолчанию скрыта работа с потоконебезопасными коллекциями, разделение на части и объединение элементов. Это отличное решение. Разработчику остается только выбирать нужные методы и следить за тем, чтобы не было зависимостей от внешних факторов.
Решение задач с помощью Stream API
Посчитаем, сколько раз объект « High » встречается в коллекции:
А теперь посмотрим, какой элемент в коллекции находится на первом месте. Если мы получили пустую коллекцию, то пусть возвращается 0 :
Благодаря методам filter и findFirst можно находить элементы, равные заданным в условии:
collection.stream().skip(collection.size() — 1).findFirst().orElse(«0») // Highload
С помощью метода skip можно искать элементы по порядку. Например, пропустить первый и вывести второй:
collection.stream().skip(1).limit(2).toArray()// [High, Load]
С максимальным значением тоже все очень просто:
Первая задача — отсортировать строки в алфавитном порядке и добавить их в массив:
collection.stream().sorted().collect(Collectors.toList()) // [f2, f4, f4, f10, f15]
А вот чуть более интересное задание — нужно выполнить сортировку в обратном алфавитному порядке и удалить дубликаты. В массиве должны оказаться только уникальные значения:
Здесь мы используем не только sorted для сортировки, но и метод distinct для удаления неуникальных значений при обработке коллекции.
Задачи про группу студентов
Теперь давайте посмотрим чуть более комплексные, взрослые задачи. Например, у нас есть коллекция, которая имеет следующий вид:
Сначала создадим коллекцию студентов и опишем их:
Теперь мы можем использовать методы стримов для обработки этой коллекции. Посчитаем средний возраст, используя метод average :
Получилась немного странная группа студентов мужского пола, но средний возраст вполне себе студенческий. Что мы здесь сделали:
Теперь давайте посмотрим, кому из наших студентов грозит получение повестки в этом году при условии, что призывной возраст установлен в диапазоне от 18 до 27 лет.
Задачи на поиск в строке
Вот как будет выглядеть код этой программы:
Программа предлагает ввести имена сотрудников. Все они сохраняются в массив ALL без предварительной обработки. Чтобы остановить ввод имен, нужно ввести пустую строку.
Сначала на экране выведется массив со всеми введенными именами. Чтобы отфильтровать их, нужно добавить условие. В нашем случае это будет первая буква — например, ‘ a ‘.
Заключение
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-ти кратной разнице при параллельной обработке.
Мы не хотим говорить вам что делать. Появление для программистов новых способов делать что-то неправильно может пугать. Ошибки в коде, архитектуре, и оценках конечно же будут случаться. Десятилетия назад некоторые люди предсказывали, что наличие параллелизма на уровне приложения приведет к большим бедам. Но это так и не сбылось.
Java Stream API: что делает хорошо, а что не очень
Настолько ли «энергичен» Java 8 Stream API? Возможно ли «превращение» обработки сложных операций над коллекциями в простой и понятный код? Где та выгода от параллельных операций, и когда стоит остановиться? Это одни из многочисленных вопросов, встречающихся читателям. Попробуем разобрать подводные камни Stream API с Тагиром Валеевым aka @lany. Многие читатели уже знакомы с нашим собеседником по статьям, исследованиям в области Java, выразительным докладам на конференциях. Итак, без проволочек, начинаем обсуждение.
— Тагир, у вас отличные показатели на ресурсе StackOverflow (gold status в ветке «java-stream»). Как вы думаете, динамика применения Java 8 Stream API и сложность конструкций выросла (на основе вопросов и ответов на данном ресурсе)?
— Верно, одно время я много времени проводил на StackOverflow, постоянно отслеживая вопросы по Stream API. Сейчас заглядываю периодически, так как, на мой взгляд, на большинство интересных вопросов уже есть ответы. Безусловно, чувствуется, что люди распробовали Stream API, было бы странно, если бы это было не так. Первые вопросы по этой теме появились ещё до выпуска Java 8, когда люди экспериментировали с ранними сборками. Расцвет пришёлся на конец 2014 и 2015-й год.
Многие интересные вопросы связаны не только с тем, что можно сделать со Stream API, но и с тем, чего нормально сделать нельзя без сторонних библиотек. Пользователи, постоянно спрашивая и обсуждая, стремились раздвинуть рамки Stream API. Некоторые из этих вопросов послужили источниками идей для моей библиотеки StreamEx, расширяющей функциональность Java 8 Stream API.
— Вы упомянули про StreamEx. Расскажите, что побудило вас к созданию? Какие цели вы преследовали?
— Мотивы были сугубо практические. Когда на работе мы перешли на Java 8, первая эйфория от красоты и удобства довольно быстро сменилась чередой спотыканий: хотелось сделать с помощью Stream API определённые вещи, которые вроде делаться должны, но по факту не получались. Приходилось удлинять код или отступать от спецификации. Я начал добавлять в рабочие проекты вспомогательные классы и методы для решения данных проблем, но выглядело это некрасиво. Потом я догадался обернуть стандартные стримы в свои классы, которые предлагают ряд дополнительных операций, и работать стало существенно приятнее. Эти классы я выделил в отдельный открытый проект и начал развивать его.
— На ваш взгляд, какие виды расчетов и операций и над какими данными действительно стоит реализовать c использованием Stream API, а что не очень подходит для обработки?
— Stream API любит неизменяемые данные. Если вы хотите поменять существующие структуры данных, а не создать новые, вам нужно что-то другое. Посмотрите в сторону новых стандартных методов (например, List.replaceAll).
Stream API любит независимые данные. Если для получения результата вам нужно использовать одновременно несколько элементов из входного набора, без сторонних библиотек будет очень коряво. Но библиотеки вроде StreamEx часто решают эту проблему.
Stream API любит решать одну задачу за проход. Если вы хотите в один обход данных решить несколько разных задач, готовьтесь писать свои коллекторы. И не факт, что это вообще получится.
Stream API не любит проверяемые исключения. Вам будет не очень удобно кидать их из операций Stream API. Опять же есть библиотеки, которые пытаются это облегчить (скажем, jOOλ), но я бы рекомендовал отказываться от проверяемых исключений.
В стандартном Stream API не хватает некоторых операций, которые очень нужны. Например, takeWhile, появится только в Java 9. Может оказаться, что вы хотите чего-то вполне разумного и несложного, но сделать это не получится. Опять же, стоит заметить, что библиотеки вроде jOOλ и StreamEx решают большинство таких проблем.
— Как вы считаете, есть ли смысл использовать parallelStream всегда? Какие проблемы могут возникнуть при «переключении» методов из stream на parallelStream?
— Ни в коем случае не надо использовать parallelStream всегда. Его надо использовать исключительно редко, и у вас должен быть хороший повод для этого.
Во-первых, большинство задач, решаемых с помощью Stream API, слишком быстрые по сравнению с накладными расходами на распределение задач по ForkJoinPool и их синхронизацию. Известная статья Дага Ли (Doug Lea) «When to use parallel streams» приводит правило большого пальца: на современных машинах обычно распараллеливать имеет смысл задачи, время выполнения которых превышает 100 микросекунд. Мои тесты показывают, что иногда и 20-микросекундная задача ускоряется от распараллеливания, но это уже зависит от многих факторов.
Во-вторых, даже если ваша задача выполняется долго, не факт, что параллелизм её ускорит. Это зависит и от качества источника, и от промежуточных операций (например, limit для упорядоченного стрима может долго работать), и от терминальных операций (скажем, forEachOrdered может иногда свести на нет выгоду от параллелизма). Самые хорошие промежуточные операции — это операции без состояния (filter, map, flatMap и peek), а самые хорошие терминальные — это семейство reduce/collect, которые ассоциативны, то есть могут эффективно разбить задачу на подзадачи и потом объединить их результаты. И то процедура объединения иногда не очень оптимальна (к примеру, для сложных цепочек groupingBy).
В-третьих, многие люди используют Stream API неверно, нарушая спецификацию. Например, передавая лямбды с внутренним состоянием (stateful) в операции вроде filter и map. Или нарушая требования к единице и ассоциативности в reduce. Не говоря уж о том, сколько неправильных коллекторов пишут. Это часто простительно для последовательных стримов, но совершенно недопустимо для параллельных. Конечно, это не повод писать неправильно, но факт налицо: параллельными стримами пользоваться сложнее, это не просто дописать parallel() где-нибудь.
И, наконец, даже если у вас стрим выполняется долго, операции в нём легко параллелятся и вы всё делаете правильно, стоит задуматься, действительно ли у вас простаивают ядра процессора, что вы готовы их отдать параллельным стримам? Если у вас веб-сервис, который постоянно загружен запросами, вполне возможно, что обрабатывать каждый запрос отдельным потоком будет разумнее. Только если у вас ядер достаточно много, либо система не загружена полностью, можно задуматься о параллельных стримах. Возможно, кстати, стоит устанавливать java.util.concurrent.ForkJoinPool.common.parallelism для ограничения параллельных стримов.
Например, если у вас 16 ядер и обычно 12 загружено, попробуйте установить уровень параллелизма 4, чтобы занять стримами оставшиеся ядра. Общих советов, конечно, нет: надо всегда проверять.
— В продолжение разговора о параллелизации, можно ли говорить о том, что на производительность влияет объем и структура данных, количество ядер процессора? Какие источники данных (например, LinkedList) не стоит обрабатывать в параллель?
— LinkedList ещё не самый худший источник. Он, по крайней мере, свой размер знает, что позволяет Stream API удачнее дробить задачи. Хуже всего для параллельности источники, которые по сути последовательны (как LinkedList) и при этом не сообщают свой размер. Обычно это то, что создано через Spliterators.spliteratorUnknownSize(), либо через AbstractSpliterator без указания размера. Примеры из JDK — Stream.iterate(), Files.list(), Files.walk(), BufferedReader.lines(), Pattern.splitAsStream() и так далее. Я говорил об этом на докладе «Странности Stream API» на JPoint в этом году. Там очень плохая реализация, которая приводит, например, к тому, что если этот источник содержит 1024 элемента или менее, то он не параллелится вообще. И даже потом параллелится довольно плохо. Для более или менее нормального параллелизма вам нужно, чтобы в нём были десятки тысяч элементов. В StreamEx реализация лучше. Например, StreamEx.ofLines(reader) (аналог BufferedReader.lines()) будет параллелиться неплохо даже для небольших файлов. Если у вас плохой источник и вы хотите его распараллелить, часто эффективнее сперва последовательно его собрать в список (например, Stream.iterate(…).collect(toList()).parallelStream()…)
Большинство стандартных структур данных из JDK являются хорошими источниками. Опасайтесь структур и обёрток из сторонних библиотек, которые совместимы с Java 7. В них не может быть переопределён метод spliterator() (потому что в Java 7 нет сплитераторов), поэтому они будут использовать реализацию Collection.spliterator() или List.spliterator() по умолчанию, которая, конечно, плохо параллелится, потому что ничего не знает о вашей структуре данных и просто оборачивает итератор. В девятке это улучшится для списков со случайным доступом.
— При использовании промежуточных операций, на ваш взгляд, какое пороговое значение их в Stream — конвейере и как это определяется? Существуют ли ограничения (явные и неявные)?
Наличие методов упорядочивания коллекций во время обработки (промежуточная операция sorted()) или упорядоченного источника данных и последующая работа с ним с помощью map, filter и reduce операций могут привести к повышению производительности?
Нет, вряд ли. Только операция distinct() использует тот факт, что вход сортирован. Она меняет алгоритм, сравнивая элемент с предыдущим, а без сортировки приходится держать HashSet. Однако для этого источник должен сообщить, что он сортирован. Все сортированные источники из JDK (BitSet, TreeSet, IntStream.range) уже содержат уникальные элементы, поэтому для них distinct() бесполезен. Ну, теоретически операция filter может что-то выиграть из-за лучшего предсказания ветвлений в процессоре, если она на первой половине набора истинна, а на второй ложна. Но если данные уже отсортированы по предикату, эффективнее не использовать Stream API, а найти границу с помощью бинарного поиска. Причём сортировка сама по себе медленная, если данные на входе плохо сортированы. Поэтому, скажем, sorted().distinct() для случайных данных будет медленнее, чем просто distinct(), хотя сам distinct() ускорится.
— Необходимо затронуть важные вопросы, связанные с отладкой кода. Вы используете метод peek(), для получения промежуточных результатов? Возможно, что у вас есть свои секреты тестирования? Поделитесь, пожалуйста, ими с читателями.
— Я почему-то не пользуюсь peek() для отладки. Если стрим достаточно сложный и что-то непонятное происходит в процессе, можно разбить его на несколько (с промежуточным списком) и посмотреть на этот список. Вообще можно привыкнуть обходить стрим в обычном пошаговом отладчике в IDE. Поначалу это страшно, но потом привыкаешь.
Когда я разрабатываю новые сплитераторы и коллекторы, я использую вспомогательные методы в тестах, которые подвергают их всестороннему тестированию, проверяя различные инварианты и запуская в разных условиях. Скажем, я не только сравниваю, что результат параллельного и последовательного стрима совпадает, а могу в параллельный стрим вставить искусственный сплитератор, который наплодит пустых фрагментов при создании параллельных задач. Они не должны влиять на результат и помогают найти нетривиальные баги. Или при тестировании сплитераторов я случайным образом дроблю их на подзадачи, которые выполняю в случайном порядке (но в одном потоке) и сверяю результат с последовательным. Это стабильный воспроизводимый тест, который хотя и однопоточный, но позволяет отловить большинство ошибок в распараллеленных сплитераторах. Вообще, крутая система тестов, которая всесторонне проверяет каждый кирпичик кода и в случае ошибок выдаёт вменяемый отчёт, обычно вполне заменяет отладку.
— Какое развитие Stream API вы видите в будущем?
— Сложный вопрос, я не умею предсказывать будущее. Сейчас многое упирается в наличие четырёх специализаций Stream API (Stream, IntStream, LongStream, DoubleStream), поэтому многий код приходится дублировать четыре раза, чего мало кому хочется. Все с нетерпением ждут специализацию дженериков, которую, вероятно, доделают в Java 10. Тогда будет проще.
Также есть проблемы с расширением Stream API. Как известно, Stream — это интерфейс, а не какой-нибудь финальный класс. С одной стороны, это позволяет расширять Stream API сторонним разработчикам. С другой стороны, добавлять новые методы в Stream API теперь не так-то легко: надо не сломать все те классы, который уже в Java 8 реализовали этот интерфейс. Каждый новый метод должен предоставить реализацию по умолчанию, выраженную в терминах существующих методов, что не всегда возможно и легко. Поэтому взрывного роста функциональности вряд ли стоит ожидать.
Самое важное, что появится в Java 9, — это методы takeWhile и dropWhile. Будут мелкие приятные штуки — Stream.ofNullable, Optional.stream, iterate с тремя аргументами и несколько новых коллекторов — flatMapping, filtering. Но, в целом, многого всё ещё будет не хватать. Зато появятся дополнительные методы в JDK, которые создают стрим: новые API теперь разрабатывают с оглядкой на стримы, да и старые подтягивают.
— Многие запомнили ваше выступление в 2015 году с докладом «Что же мы измеряем?». В этом году вы планируете выступить с новой темой на Joker? О чем пойдет речь?
— Я решил делать новый доклад, который не очень творчески назову «Причуды Stream API». Это будет в некотором смысле продолжение доклада «Странности Stream API» с JPoint: я расскажу о неожиданных эффектах производительности и скользких местах Stream API, акцентируя внимание на том, что будет исправлено в Java 9.
— Спасибо большое за интересные и подробные ответы. С нетерпением ждем ваше новое выступление.
Прикоснуться к миру Stream API и другого Java-хардкора можно будет на конференции Joker 2016. Там же — вопросы спикерам, дискуссии вокруг докладов и бесконечный нетворкинг.