Что такое параллельные стримы?
Создаются двумя способами:
🟢 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
- I/O-операции — блокируют воркеров ForkJoinPool, остальные задачи ждут
- Несколько тысяч элементов — overhead > выгода
- Stateful операции с ThreadLocal — воркеры переиспользуются, данные «протекают»
- Когда важен порядок — parallelStream не гарантирует порядок (кроме упорядоченных источников)
🟡 Middle Level
Механизм: ForkJoin и Spliterator
Параллельные стримы используют фреймворк ForkJoin:
- Данные разделяются на части через
Spliterator.trySplit() - Каждая часть обрабатывается отдельным потоком
- Результаты объединяются (
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 окупается
Когда параллелизм ВРЕДИТ:
- Маленькие коллекции (оверхед на разделение/слияние)
- Дешевые операции (быстрее переключение контекста)
- Блокировки (синхронизация убивает параллелизм)
- 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]]