Parallel Stream и ForkJoinPool
collection.parallelStream() и stream.parallel() выполняют операции параллельно, обычно через ForkJoinPool.commonPool(). Параллелизм common pool обычно близок к числу доступных процессоров минус один, но может быть настроен; точное значение возвращает ForkJoinPool.getCommonPoolParallelism().
long total = values.parallelStream()
.filter(this::isValid)
.mapToLong(this::weight)
.sum();Когда возможен выигрыш
- много элементов;
- операция на элемент CPU-bound и достаточно дорогая;
- источник хорошо делится, например массив или
ArrayList; - нет общего mutable state и блокирующего I/O;
- итоговая операция хорошо объединяется.
Когда будет хуже
List<Integer> result = new ArrayList<>();
values.parallelStream().forEach(result::add); // data raceДаже синхронизированный список устранит повреждение, но общая блокировка съест параллелизм. Правильно собрать результат через stream collector.
forEach не сохраняет encounter order; forEachOrdered сохраняет, но ограничивает параллелизм. findAny лучше подходит параллельному поиску, чем findFirst, если порядок не важен.
Common pool разделяется с другими parallel streams и CompletableFuture без явно переданного executor. Один долгий или блокирующий pipeline способен повлиять на несвязанный код. Поэтому parallel stream — решение после измерения, а не переключатель «сделать быстрее».
На собеседовании
- Какой пул обычно использует parallel stream?
ForkJoinPool.commonPool. - Thread-safe ли lambda автоматически? Нет.
- Гарантирует ли
forEachпорядок? Нет; для этогоforEachOrdered. - Почему маленький список может стать медленнее? Разбиение и координация дороже вычисления.
- Стоит ли делать сетевые вызовы внутри parallel stream? Обычно нет: блокируется общий ограниченный пул и теряется контроль над backpressure.