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: Неучёт накладных расходов. Запуск потоков, переключение между ними, сбор результатов — всё это требует времени. Для маленьких задач это время может быть больше, чем время самой работы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ