JavaRush /Курси /JAVA 25 SELF /Вступ до паралелізму

Вступ до паралелізму

JAVA 25 SELF
Рівень 54 , Лекція 0
Відкрита

1. Багатопоточність vs Паралелізм

Багатопоточність: багато, але не обов’язково одночасно

Багатопоточність — це коли у вашій програмі є кілька потоків виконання. Кожен потік — як окрема лінія дій: один щось обчислює, інший очікує введення користувача, третій зберігає дані у файл. У Java ви створюєте потоки через клас Thread, реалізуєте інтерфейс Runnable або використовуєте високорівневі інструменти на кшталт ExecutorService (про них — у наступній лекції).

АЛЕ! Багатопоточність не гарантує, що ваші завдання справді виконуються одночасно. Усе залежить від того, скільки ядер у вашому процесорі. Якщо ядро одне, то потоки просто швидко «перемикаються» між собою — так швидко, що людині здається, ніби все відбувається одночасно. Насправді процесор виконує лише один потік у кожен момент часу, а решта чекають своєї черги.

Паралелізм: коли завдання справді виконуються одночасно

Паралелізм — це коли ваш код справді виконується одночасно на кількох ядрах процесора. Якщо у вас сучасний комп’ютер із 4, 8, 16 ядрами — ви можете реально прискорити обробку великих завдань, розбивши їх на незалежні частини та розподіливши між ядрами.

Якщо провести аналогію, то багатопоточність — це коли у вас один кухар, який швидко перемикається між приготуванням борщу, смаженням котлет і нарізанням салату. Паралелізм — це коли у вас одразу кілька кухарів, і кожен відповідає за свою страву.

У чому різниця на практиці?

Багатопоточність — це про зручність і швидку реакцію. Ви використовуєте кілька потоків, щоб програма не «зависала»: один потік чекає мережу, інший малює інтерфейс, третій щось рахує. Усе працює паралельно за відчуттями, але не обов’язково одночасно.

Паралелізм — це про швидкість. Тут справді кілька ядер процесора виконують різні частини завдання одночасно, щоб отримати результат швидше.

Іншими словами: багатопоточність допомагає організувати роботу, а паралелізм — прискорити її.

Важливо:
Багатопоточність потрібна завжди, коли є завдання, які можна виконувати незалежно.
Паралелізм потрібен, коли ви хочете пришвидшити обчислення за рахунок реального розподілу роботи між ядрами.

Приклад: обробка великого масиву

Уявімо, що у нас є масив із 10 мільйонів чисел, і ми хочемо порахувати суму всіх елементів.

Послідовно:
Один потік проходить по всьому масиву та рахує суму. Просто й надійно, але довго.

Багатопоточно (але на одному ядрі):
Ви ділите масив на 4 частини, створюєте 4 потоки, кожен рахує свою частину. Але якщо у вас лише одне ядро, потоки просто по черзі працюватимуть — прискорення не буде, а накладні витрати на перемикання потоків можуть навіть сповільнити програму.

Паралельно (на кількох ядрах):
Ви ділите масив на 4 частини, запускаєте 4 потоки, і кожен потік справді працює на своєму ядрі. Підсумкова сума збирається з 4 частин. Це справді швидше — особливо на великих обсягах даних.

Зате реалізувати послідовну обробку масиву дуже просто — ви вже багато разів писали подібні програми:

// Приклад: послідовна обробка масиву
int[] arr = new int[10_000_000];
// ... заповнення масиву ...
long sum = 0;
for (int x : arr) {
    sum += x;
}
System.out.println(sum);

Багатопоточна та паралельна версії — трохи складніші, ми їх розглядатимемо на наступних лекціях за допомогою сучасних інструментів.

2. Навіщо потрібен паралелізм

Сучасні процесори давно вийшли за межі одного ядра. Навіть ваш смартфон, швидше за все, має не менше чотирьох, а настільні комп’ютери й сервери — вісім, шістнадцять, тридцять дві і більше. Якщо застосунок уміє використовувати всі ці ядра, він здатен працювати у рази швидше.

Раніше продуктивність процесорів зростала за рахунок збільшення тактової частоти — приблизно до середини 2000-х це справді працювало. Але зростання частот уперлося у фізичні обмеження, і тоді почалася нова ера — багатопроцесорні та багатоядерні системи. Тепер виграють ті програми, які вміють ефективно розподіляти роботу між ядрами.

Де паралелізм справді пришвидшує?

  • Обробка великих даних: аналіз логів, статистика, агрегація — усе, що можна розбити на незалежні шматки.
  • Рендеринг, обробка зображень, відео: кожен піксель або фрагмент можна обробляти окремо.
  • Обчислення в науці, моделювання: математичні завдання, симуляції, навчання моделей.
  • Серверні застосунки: одночасне обслуговування багатьох клієнтів.
  • Реактивні застосунки: коли потрібно швидко реагувати на безліч подій, не блокуючи основний потік.

Коли паралелізм не допомагає?

  • Якщо завдання маленьке, накладні витрати на запуск паралелізму можуть бути більшими, ніж виграш.
  • Якщо завдання не можна розбити на незалежні частини (наприклад, коли кожен крок залежить від попереднього).
  • Коли є багато спільних ресурсів (наприклад, один і той самий файл), і потоки починають заважати одне одному.

3. Типові завдання для паралелізму

Розгляньмо, які завдання найчастіше «роздають» по ядрах.

Масивні обчислення

  • Підсумовування, пошук максимуму/мінімуму, підрахунок статистик за великим масивом.
  • Приклад: обчислити середнє значення температури за мільйоном датчиків.

Обробка колекцій

  • Фільтрація, сортування, перетворення великих списків (наприклад, обробка замовлень інтернет-магазину).
  • Приклад: вибрати всі замовлення дорожчі за 10 000 рублів і відсортувати їх за датою.

Рендеринг і обробка графіки

  • Застосувати фільтр до всіх пікселів зображення (наприклад, зробити чорно-білим).
  • Кожен піксель можна обробляти незалежно — ідеальний випадок для паралелізму.

Аналіз даних, big data

  • MapReduce, агрегація, підрахунок статистик за величезними обсягами даних.
  • Приклад: обробка логів за рік для пошуку аномалій.

Приклад: паралельний підрахунок суми
Нехай у нас є масив із 1 мільйона чисел. Можна розбити його на 4 частини і порахувати суму кожної частини в окремому потоці, а потім скласти результати.

4. Проблеми та виклики паралелізму

Складність налагодження
Коли код працює в кількох потоках, помилки можуть виявлятися лише у рідкісних випадках, коли потоки «перетинаються» особливим чином. Іноді помилка з’являється раз на 1000 запусків — і впіймати її дуже складно.

Гонки даних (race condition)
Якщо кілька потоків одночасно змінюють одну й ту саму змінну або об’єкт — можливі некоректні результати. Наприклад, два потоки одночасно збільшують лічильник, і підсумкове значення виявляється меншим за очікуване.

Синхронізація
Щоб уникнути гонок, потрібно синхронізувати доступ до спільних даних — через ключове слово synchronized, блокування, атомарні змінні та інші інструменти. Це ускладнює код і може призвести до інших проблем (наприклад, deadlock — взаємне блокування потоків).

Балансування навантаження
Якщо ви розбили завдання на 4 частини, а одна з них виявилася набагато важчою за інші — три потоки вже закінчили роботу і простоюють, а четвертий усе ще працює. У підсумку прискорення немає.

Накладні витрати
Запуск потоків, перемикання між ними, синхронізація — усе це потребує часу. Якщо завдання маленьке, то паралелізм лише сповільнить виконання.

Таблиця: Порівняння підходів

Підхід Коли працює швидко Коли гальмує Приклад застосування
Послідовний (1 потік) Малі завдання, проста логіка Великі обсяги даних Обробка 10 рядків
Багатопоточність (на 1 ядрі) Асинхронні завдання (очікування IO) CPU-bound завдання (упираються в обчислення процесора) на 1 ядрі Одночасне завантаження файлів
Паралелізм (багато ядер) Великі, незалежні завдання Малі завдання, тісні залежності Обробка великого масиву

Візуалізація: як це виглядає

// Послідовна обробка (1 потік)
[Завдання 1][Завдання 2][Завдання 3][Завдання 4]

// Багатопоточність на одному ядрі (логіка перемикання)
[Завдання 1] [Завдання 2] [Завдання 3] [Завдання 4]
(але насправді одночасно працює лише одна, решта чекають)

// Паралелізм на чотирьох ядрах
[Завдання 1]    [Завдання 2]    [Завдання 3]    [Завдання 4]
(усі виконуються одночасно)

5. Типові помилки під час спроб паралелізації

Помилка № 1: Паралелізувати усе підряд. Багато новачків думають: «Чим більше потоків — тим швидше!». Насправді це не так. Якщо завдань мало або вони надто прості — виграш відсутній, а іноді програма навіть працює повільніше.

Помилка № 2: Ігнорування синхронізації. Якщо кілька потоків працюють з одними й тими самими даними без синхронізації — отримаєте гонки даних, порушення логіки та помилки, які складно спіймати.

Помилка № 3: Паралелізм заради паралелізму. Паралелізм — це не самоціль. Він потрібен, коли є реальні завдання, які можна ефективно розбити на незалежні частини.

Помилка № 4: Неврахування особливостей завдання. Деякі завдання взагалі не можна розпаралелити (наприклад, коли крок N+1 залежить від результату кроку N). У таких випадках паралелізм не дасть переваги.

Помилка № 5: Неврахування накладних витрат. Запуск потоків, перемикання між ними, збирання результатів — усе це потребує часу. Для маленьких завдань цей час може бути більшим, ніж час самої роботи.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ