Коли використовувати parallel streams?
Використовуйте паралельні стріми, коли потрібно обробити багато даних і задача навантажує процесор (CPU).
🟢 Junior Level
Використовуйте паралельні стріми, коли потрібно обробити багато даних і задача навантажує процесор (CPU).
Коли ТАК:
- Обробка великих колекцій (тисячі елементів)
- Складні обчислення (математика, хешування)
- Кожен елемент обробляється незалежно
Коли НІ:
- Маленькі списки (менше ~100 елементів) — overhead на створення ForkJoinTask (~50мкс) перевищує час послідовної обробки (~1мкс).
- Прості операції (
x + 1) - Запити до бази даних або HTTP
// ДОБРЕ — CPU-інтенсивна задача
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 |
Коли ВАРТО використовувати
- CPU-інтенсивні задачі: математика, криптографія, обробка зображень
- Незалежні операції: елементи не впливають один на одного
- Проста редукція:
sum,min,max— асоціативні операції
Коли НЕ ВАРТО
- I/O операції: запити до БД, HTTP — блокують commonPool
- Stateful операції:
limit(),sorted(),distinct()потребують координації - Маленькі дані: overhead на розділення/зливання більший за обчислення
- 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 операціями]]