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

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

Используйте параллельные стримы, когда нужно обработать много данных и задача нагружает процессор (CPU).

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

🟢 Junior Level

Используйте параллельные стримы, когда нужно обработать много данных и задача нагружает процессор (CPU).

Когда ДА:

  • Обработка больших коллекций (тысячи элементов)
  • Сложные вычисления (математика, хеширование)
  • Каждый элемент обрабатывается независимо

Когда НЕТ:

  • Маленькие списки (менее ~100 элементов) — overhead на создание ForkJoinTask (~50мкс) превышает время последовательной обработки (~1мкс).
  • Простые операции (x + 1)
  • Запросы к базе данных или HTTP
// ХОРОШО — CPU-intensive задача
bigList.parallelStream()
    .map(this::heavyComputation)
    .collect(toList());

// ПЛОХО — I/O операция
bigList.parallelStream()
    .map(this::saveToDatabase)  // блокирует потоки!
    .collect(toList());

🟡 Middle Level

Модель N*Q

Формула от экспертов Oracle:

  • N — количество элементов
  • Q — объем работы (CPU cycles) на элемент
  • Если N * Q > 10,000 — параллелизм даст выигрыш

Примеры:

  • Суммирование 100 чисел (Q мало) — parallel медленнее
  • Хеширование 100 больших документов (Q велико) — parallel быстрее

Характеристики источника

Источник Делимость Почему
ArrayList, Массивы Отлично Делятся по индексу за O(1)
IntStream.range Отлично Начало и конец известны
HashSet, TreeSet Средне Структура сложнее
LinkedList Плохо Нужно пройти половину списка
Stream.iterate Хуже всего Элемент N зависит от N-1

Когда СТОИТ использовать

  1. CPU-интенсивные задачи: математика, криптография, обработка изображений
  2. Независимые операции: элементы не влияют друг на друга
  3. Простая редукция: sum, min, max — ассоциативные операции

Когда НЕ СТОИТ

  1. I/O операции: запросы к БД, HTTP — блокируют commonPool
  2. Stateful операции: limit(), sorted(), distinct() требуют координации
  3. Маленькие данные: оверхед на разделение/слияние больше вычисления
  4. Side Effects: изменение внешних переменных требует синхронизации

ParallelStream vs альтернативы

  • ExecutorService.invokeAll() — больше контроля, но больше boilerplate
  • CompletableFuture.allOf() — лучше для I/O-bound задач с неблокирующим ожиданием
  • Parallel Arrays (библиотеки типа fastutil) — оптимизированы для примитивов
  • parallelStream — лучший выбор для CPU-bound операций над коллекциями

🔴 Senior Level

False Sharing

При параллельной обработке массивов примитивов потоки могут конфликтовать за кэш-линии процессора (L1/L2 cache), если обновляют данные, лежащие слишком близко.

GC Pressure

Параллельные стримы создают много мелких задач (RecursiveTask), что увеличивает частоту Minor GC в высоконагруженных системах.

Common Pool Poisoning

В Java 21 поведение ForkJoinPool.commonPool() изменилось. Всегда тестируйте parallelStream на вашей версии JVM.

Один стрим с блокирующими операциями может занять все потоки commonPool — все остальные параллельные стримы в приложении встанут.

Диагностика

JMH (Java Microbenchmark Harness): Никогда не внедряйте parallelStream без замера через JMH. Интуиция подводит в вопросах многопоточности.

Настройка пула: -Djava.util.concurrent.ForkJoinPool.common.parallelism=N — влияет на ВСЁ приложение.

Проверка: Всегда сравнивайте производительность stream() vs parallelStream() на реальных данных.


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

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

  • Правило N * Q > 10,000: N — количество элементов, Q — стоимость вычислений на элемент
  • ДА: CPU-интенсивные задачи (математика, хеширование, обработка изображений), независимые элементы, простые редукции
  • НЕТ: I/O операции, маленькие данные (< ~100 элементов), stateful операции (limit, sorted, distinct), side effects
  • Отличная делимость: ArrayList, массивы, IntStream.range. Плохая: LinkedList, Stream.iterate
  • parallelStream vs альтернативы: CompletableFuture для I/O-bound, ExecutorService для контроля
  • Всегда замерять через JMH — интуиция подводит в многопоточности

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

  • Почему small collections не подходят? — Overhead на ForkJoinTask (~50мкс) > последовательная обработка (~1мкс)
  • Что такое False Sharing? — Потоки конфликтуют за кэш-линии процессора при близком расположении данных
  • Common Pool Poisoning — что это? — Один стрим с блокирующими I/O занимает все потоки, остальные стримы ждут
  • Java 21 и parallelStream — что изменилось? — Поведение ForkJoinPool.commonPool() изменилось, нужно тестировать

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

  • «parallelStream ускорит запросы к БД» — нет, I/O блокирует потоки и замедляет всё приложение
  • «Не нужно тестировать — параллелизм всегда быстрее» — обязательно JMH на реальных данных
  • «-D ForkJoinPool.common.parallelism влияет только на мой стрим» — влияет на ВСЁ приложение
  • «parallelStream подходит для всего» — только для CPU-bound операций над коллекциями

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

  • [[9. Что такое параллельные стримы]]
  • [[1. Какие преимущества даёт использование Stream API]]
  • [[5. Что делает операция collect()]]
  • [[2. В чём разница между intermediate и terminal операциями]]