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.