Вопрос 9 · Раздел 8

Что такое параллельные стримы?

Создаются двумя способами:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Параллельные стримы — это способ обработать данные в несколько потоков одновременно, используя ForkJoinPool.commonPool(), который по умолчанию задействует ядра - 1 (одно ядро остаётся для системных задач)

Создаются двумя способами:

// Из коллекции
list.parallelStream().forEach(System.out::println);

// Из обычного стрима
list.stream().parallel().forEach(System.out::println);

По умолчанию использует все доступные ядра CPU. Для списка из 1000 элементов обработка теоретически может идти быстрее пропорционально числу воркеров (cores - 1), но на практике overhead на fork/join и merge снижает ускорение до 2-4x

Важно: Порядок элементов не гарантируется в forEach.

Когда НЕ использовать parallel streams

  1. I/O-операции — блокируют воркеров ForkJoinPool, остальные задачи ждут
  2. Несколько тысяч элементов — overhead > выгода
  3. Stateful операции с ThreadLocal — воркеры переиспользуются, данные «протекают»
  4. Когда важен порядок — parallelStream не гарантирует порядок (кроме упорядоченных источников)

🟡 Middle Level

Механизм: ForkJoin и Spliterator

Параллельные стримы используют фреймворк ForkJoin:

  1. Данные разделяются на части через Spliterator.trySplit()
  2. Каждая часть обрабатывается отдельным потоком
  3. Результаты объединяются (combiner)

Эффективность зависит от источника:

  • ArrayList, массивы — идеально делятся по индексу
  • HashSet, TreeSet — делятся неплохо
  • LinkedList — ужасно (нужно пройти половину списка)
  • Stream.iterate — невозможно распараллелить

ForkJoinPool.commonPool()

По умолчанию все параллельные стримы используют один общий пул:

  • Размер = число_ядер - 1
  • Риск: Если один стрим выполняет блокирующие I/O, он занимает потоки общего пула — все остальные параллельные стримы ждут

Порядок выполнения

// Случайный порядок
parallelStream().forEach(System.out::println);

// Гарантированный порядок (медленнее)
parallelStream().forEachOrdered(System.out::println);

🔴 Senior Level

Модель N*Q

Эмпирическое правило: параллелизм выгоден при N * Q > 10,000:

  • N — количество элементов
  • Q — стоимость вычислений на элемент
  • 10,000 — примерный порог, когда overhead fork/join окупается

Когда параллелизм ВРЕДИТ:

  1. Маленькие коллекции (оверхед на разделение/слияние)
  2. Дешевые операции (быстрее переключение контекста)
  3. Блокировки (синхронизация убивает параллелизм)
  4. I/O операции (блокируют commonPool)

Stateful Operations в параллелизме

sorted(), distinct(), limit() в параллельном стриме требуют полной синхронизации (“барьер”), что часто делает их медленнее последовательного режима.

ThreadLocal Danger

В general case не полагайтесь на ThreadLocal — воркеры ForkJoinPool переиспользуются между задачами. Если контролируете кастомный ForkJoinPool и очищаете ThreadLocal в finally — допустимо.

Custom ForkJoinPool

Для изоляции нагрузки используйте кастомный пул:

ForkJoinPool customPool = new ForkJoinPool(4);
long result = customPool.submit(() ->
    list.parallelStream().mapToInt(this::doWork).sum()
).get();

Стрим использует текущий пул потока, а не commonPool.

Диагностика

  • Thread Names: Внутри лямбды выведите Thread.currentThread().getName() — увидите ForkJoinPool.commonPool-worker-N
  • JFR (Java Flight Recorder): Покажет активность потоков в ForkJoinPool
  • -Djava.util.concurrent.ForkJoinPool.common.parallelism=N: Системный флаг для настройки размера пула

🎯 Шпаргалка для интервью

Обязательно знать:

  • Параллельные стримы используют ForkJoinPool.commonPool() (размер = ядра - 1)
  • Два способа создания: collection.parallelStream() и stream().parallel()
  • Механизм: Spliterator.trySplit() разделяет данные, каждый воркер обрабатывает свою часть
  • Эффективность деления: ArrayList/массивы > HashSet/TreeSet > LinkedList > Stream.iterate
  • Эмпирическое правило: N * Q > 10,000 — параллелизм выгоден
  • Порядок не гарантируется в forEach, но гарантирован в forEachOrdered

Частые уточняющие вопросы:

  • Почему I/O операции опасны в parallelStream? — Блокируют воркеров commonPool, все стримы в приложении встанут
  • Когда параллелизм ВРЕДИТ? — Маленькие коллекции, дешёвые операции, блокировки, I/O, stateful операции
  • Как изолировать нагрузку? — Использовать кастомный ForkJoinPool: customPool.submit(() -> list.parallelStream()...)
  • Почему LinkedList ужасен для параллелизма? — Нужно пройти половину списка для разделения

Красные флаги (НЕ говорить):

  • «parallelStream всегда быстрее» — нет, оверхед на fork/join/merge может замедлить
  • «parallelStream создаёт новые потоки» — нет, использует общий ForkJoinPool.commonPool()
  • «forEach в parallelStream гарантирует порядок» — нет, только forEachOrdered
  • «ThreadLocal безопасен в ForkJoinPool» — нет, воркеры переиспользуются между задачами

Связанные темы:

  • [[10. Когда использовать parallel streams]]
  • [[1. Какие преимущества даёт использование Stream API]]
  • [[2. В чём разница между intermediate и terminal операциями]]
  • [[6. Что такое Collector и какие есть встроенные Collectors]]