0Pricing
Java Academy · Урок

Параллельные потоки: производительность и подводные камни

Включайте параллельные потоки, разбирайтесь в общем пуле потоков и избегайте ошибок из-за общего изменяемого состояния.

«Параллельные потоки: производительность и подводные камни» — бесплатный урок Java Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.

Включение параллельных потоков

Вызовите .parallel() для любого потока или используйте Collection.parallelStream(). Операции потока выполняются в общем ForkJoinPool (по умолчанию: число ядер CPU - 1 поток).

long count = list.parallelStream()
    .filter(n -> n % 2 == 0)
    .count();
System.out.println(count);

Когда параллельные потоки эффективны

Параллельные потоки оправданы, если: набор данных большой (100 000+ элементов), обработка каждого элемента требует значительных вычислительных ресурсов, а конвейер не имеет состояния и не зависит от порядка.

// Good candidate: CPU-heavy computation on large dataset
long sum = LongStream.rangeClosed(1, 10_000_000)
    .parallel()
    .filter(n -> isPrime(n))
    .sum();
System.out.println(sum);

Когда НЕ следует использовать параллельные потоки

Избегайте параллельных потоков для небольших коллекций, операций, ограниченных вводом-выводом (они блокируют потоки ForkJoinPool), операций с состоянием (сортировка, удаление дубликатов) или ситуаций, когда порядок важен и его восстановление требует значительных затрат.

// Bad: I/O bound — blocking ForkJoinPool threads starves other tasks
List<String> result = urls.parallelStream()
    .map(url -> httpGet(url)) // BLOCKS the common pool
    .collect(Collectors.toList());

Ошибка из-за общего изменяемого состояния

Параллельные потоки выполняют операции одновременно в нескольких потоках. Изменение общей изменяемой коллекции приводит к состояниям гонки и неверным результатам.

// RACE CONDITION — never do this:
List<Integer> results = new ArrayList<>(); // not thread-safe
numbers.parallelStream().forEach(n -> results.add(n)); // corrupts list!
// Fix:
List<Integer> safe = numbers.parallelStream().collect(Collectors.toList());

Операции с состоянием: sorted и удаление дубликатов

sorted() и distinct() требуют увидеть все элементы до формирования результата, что ограничивает параллелизм и часто делает параллельное выполнение медленнее последовательного.

// sorted() forces collect-all, then sort — parallel overhead usually not worth it:
list.parallelStream().sorted().collect(Collectors.toList());

Операции, чувствительные к порядку

findFirst() и forEachOrdered() сохраняют порядок обхода в параллельных потоках, что приводит к дополнительным затратам на синхронизацию. Используйте findAny() или forEach(), если порядок не важен.

// Faster in parallel (order-insensitive):
Optional<Integer> any = list.parallelStream().filter(n -> n > 10).findAny();
// Slower in parallel (must preserve order):
Optional<Integer> first = list.parallelStream().filter(n -> n > 10).findFirst();

Выбор размера пула потоков

Общий ForkJoinPool использует Runtime.getRuntime().availableProcessors() - 1 потоков. Запускайте параллельные потоки в пользовательском пуле, выполняя их внутри вызова ForkJoinPool.invoke().

ForkJoinPool custom = new ForkJoinPool(8);
long result = custom.submit(
    () -> LongStream.rangeClosed(1, 1_000_000).parallel().sum()
).get();

Сравнение производительности параллельного и последовательного выполнения

Всегда выполняйте измерения с помощью JMH на реалистичных объёмах данных. Накладные расходы параллельной обработки (разделение, координация потоков, объединение) оправданы только тогда, когда время вычислений значительно превышает эти расходы.

@Benchmark
public long sequential() { return LongStream.rangeClosed(1,1_000_000).sum(); }
@Benchmark
public long parallel()   { return LongStream.rangeClosed(1,1_000_000).parallel().sum(); }

Операции свёртки с параллельной обработкой

reduce() и collect() предназначены для корректной работы в параллельном режиме, если операции ассоциативны, а нейтральное значение выбрано правильно.

// Associative reduce — safe in parallel:
int sum = list.parallelStream().reduce(0, Integer::sum);
// Non-associative: subtraction — NOT safe in parallel:
int bad = list.parallelStream().reduce(0, (a, b) -> a - b); // wrong result!

Важность возможности разделения

Параллельные потоки разделяют источник данных с помощью Spliterator. ArrayList и массивы разделяются за O(1); LinkedList и HashSet разделяются плохо, что снижает эффективность параллельной обработки.

Итоги: список проверок для параллельных потоков

Перед использованием параллельной обработки убедитесь в следующем: (1) большой набор данных, (2) операции, ограниченные CPU, (3) отсутствие общего изменяемого состояния, (4) порядок не имеет значения, (5) источник допускает разделение (массив/ArrayList). Если сомневаетесь, выполните измерения.

Быстрая проверка

Что происходит, когда вы вызываете add для коллекции, не безопасной для многопоточности, в forEach параллельного потока?

Итоги

Параллельные потоки используют ForkJoinPool. Они эффективны для больших наборов данных, операций, ограниченных 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 — локальная установка не требуется.

Все уроки этого курса

  1. flatMap для вложенных коллекций
  2. Параллельные потоки: производительность и подводные камни
  3. Spliterator: разделение для параллельной обработки
  4. Бесконечные потоки с iterate и generate
← Назад к Java Academy