Питання 10 · Розділ 8

Коли використовувати parallel streams?

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

Мовні версії: English Russian Ukrainian

🟢 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

Коли ВАРТО використовувати

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

Коли НЕ ВАРТО

  1. I/O операції: запити до БД, HTTP — блокують commonPool
  2. Stateful операції: limit(), sorted(), distinct() потребують координації
  3. Маленькі дані: overhead на розділення/зливання більший за обчислення
  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 операціями]]