Когда параллелизм помогает
Учитывайте нагрузку и размер данных
«Когда параллелизм помогает» — бесплатный урок Java Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.
Параллелизм имеет цену
Переход к параллельному выполнению добавляет накладные расходы: разделение данных, распределение задач и объединение результатов. Он окупается только тогда, когда эти расходы меньше сэкономленного времени.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long sum = IntStream.rangeClosed(1, 10_000_000)
.parallel()
.asLongStream()
.sum();
System.out.println(sum);
}
}Фактор 1: размер данных (N)
Большое значение N позволяет распределить фиксированные накладные расходы параллелизма на большее число элементов. Приблизительное практическое правило: параллельное выполнение начинает окупаться при десятках тысяч элементов.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long count = IntStream.rangeClosed(1, 5_000_000)
.parallel()
.filter(n -> n % 7 == 0)
.count();
System.out.println(count);
}
}Фактор 2: объём работы на элемент (Q)
Важна стоимость Q обработки каждого элемента. Большой объём работы на элемент (сложные вычисления) приносит пользу от параллелизма даже при меньшем N.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long primes = IntStream.rangeClosed(2, 200_000)
.parallel()
.filter(Main::isPrime)
.count();
System.out.println(primes);
}
static boolean isPrime(int n) {
for (int i = 2; (long) i * i <= n; i++)
if (n % i == 0) return false;
return true;
}
}Интуитивное понимание произведения N на Q
Представляйте общий объём полезной работы как N x Q. Чем больше это произведение, тем сильнее может помочь параллелизм. Малое N или тривиальное Q редко оправдывают его использование.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
double sum = IntStream.rangeClosed(1, 1_000_000)
.parallel()
.mapToDouble(n -> Math.sqrt(n) * Math.log(n + 1))
.sum();
System.out.println(sum);
}
}Фактор 3: возможность разделения
Источники данных, которые легко и равномерно разделяются, хорошо подходят для параллельной обработки: массивы, ArrayList и IntStream.range. LinkedList и источники на основе итераторов разделяются плохо.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) list.add(i);
long even = list.parallelStream().filter(n -> n % 2 == 0).count();
System.out.println(even);
}
}Хороший источник: массивы и диапазоны
Диапазоны примитивных значений имеют известный размер и разделяются за O(1), поэтому идеально подходят как параллельные источники.
import java.util.stream.LongStream;
public class Main {
public static void main(String[] args) {
long sum = LongStream.rangeClosed(1, 20_000_000)
.parallel()
.sum();
System.out.println(sum);
}
}Плохой источник: итеративные генераторы
Stream.iterate по определению создаёт элементы последовательно, поэтому не может разделиться, пока элементы не будут материализованы. Это слабый источник для параллельной обработки.
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
long count = Stream.iterate(1, n -> n + 1)
.limit(1_000_000)
.parallel()
.filter(n -> n % 2 == 0)
.count();
System.out.println(count);
}
}Фактор 4: дешёвый этап объединения
Параллелизму нужен недорогой способ объединять частичные результаты. sum и count объединяются тривиально, а создание отсортированного списка или сборщика со сложной древовидной структурой требует больших затрат.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
int max = IntStream.rangeClosed(1, 5_000_000)
.parallel()
.map(n -> n % 1000)
.max()
.getAsInt();
System.out.println(max);
}
}Избегайте задач, ограниченных вводом-выводом
Параллельные потоки предназначены для задач, ограниченных производительностью CPU, и выполняются в пуле fork-join. Блокирующий ввод-вывод истощает общий пул и вредит всему приложению. Вместо этого используйте отдельные исполнители для операций ввода-вывода.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
long total = IntStream.rangeClosed(1, 1_000_000)
.parallel()
.mapToLong(n -> (long) n * n)
.sum();
System.out.println(total);
}
}Измеряйте, а не предполагайте
Единственный надёжный способ узнать, помогает ли параллелизм, — измерить производительность на реалистичных данных. Интуитивные представления о производительности часто ошибочны.
import java.util.stream.LongStream;
public class Main {
public static void main(String[] args) {
long start = System.nanoTime();
long sum = LongStream.rangeClosed(1, 50_000_000).parallel().sum();
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("sum=" + sum + " took ~" + ms + "ms");
}
}Практический список проверок
Выбирайте параллельное выполнение, если выполняются все условия:
- Большое N и/или большой объём работы на элемент.
- Источник, допускающий разделение (массив, ArrayList, диапазон).
- Дешёвое ассоциативное объединение.
- Задача ограничена CPU и не содержит блокирующего ввода-вывода.
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
double avg = IntStream.rangeClosed(1, 10_000_000)
.parallel()
.mapToDouble(Math::sqrt)
.average()
.getAsDouble();
System.out.println(avg);
}
}Быстрая проверка
Какой сценарий лучше всего подходит для параллельного потока?
Итоги
Вы узнали, когда параллелизм приносит пользу:
- Польза растёт вместе с N x Q (размером данных, умноженным на стоимость обработки одного элемента).
- Нужен источник, допускающий разделение (массивы, ArrayList, диапазоны).
- Нужно дешёвое ассоциативное объединение.
- Задача должна быть ограничена CPU; избегайте блокирующего ввода-вывода и всегда проводите измерения.
Часто задаваемые вопросы
Урок «Когда параллелизм помогает» бесплатный?
Да — полный текст урока «Когда параллелизм помогает» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.
Чему я научусь в уроке «Когда параллелизм помогает»?
Учитывайте нагрузку и размер данных Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Java Academy?
Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Когда параллелизм помогает»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Java Academy?
Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Создание параллельных потоков
- Когда параллелизм помогает
- Потокобезопасность и побочные эффекты
- Распространённые ошибки