Що таке паралельні стріми?
Створюються двома способами:
🟢 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 завжди швидший» — ні, overhead на fork/join/merge може сповільнити
- «parallelStream створює нові потоки» — ні, використовує спільний ForkJoinPool.commonPool()
- «forEach в parallelStream гарантує порядок» — ні, лише forEachOrdered
- «ThreadLocal безпечний у ForkJoinPool» — ні, воркери перевикористовуються між задачами
Пов’язані теми:
- [[10. Коли використовувати parallel streams]]
- [[1. Які переваги дає використання Stream API]]
- [[2. В чому різниця між intermediate та terminal операціями]]
- [[6. Що таке Collector і які є вбудовані Collectors]]