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

Що таке паралельні стріми?

Створюються двома способами:

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

🟢 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

  1. I/O-операції — блокують воркерів ForkJoinPool, решта задач чекають
  2. Кілька тисяч елементів — overhead > вигода
  3. Stateful операції з ThreadLocal — воркери перевикористовуються, дані «витікають»
  4. Коли важливий порядок — parallelStream не гарантує порядок (крім впорядкованих джерел)

🟡 Middle Level

Механізм: ForkJoin та Spliterator

Паралельні стріми використовують фреймворк ForkJoin:

  1. Дані розділяються на частини через Spliterator.trySplit()
  2. Кожна частина обробляється окремим потоком
  3. Результати об’єднуються (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 окупається

Коли паралелізм ЗАВАЖАЄ:

  1. Маленькі колекції (оверхед на розділення/зливання)
  2. Дешеві операції (швидше перемикання контексту)
  3. Блокування (синхронізація вбиває паралелізм)
  4. 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]]