Параллельные потоки: производительность и подводные камни
Включайте параллельные потоки, разбирайтесь в общем пуле потоков и избегайте ошибок из-за общего изменяемого состояния.
«Параллельные потоки: производительность и подводные камни» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- flatMap для вложенных коллекций
- Параллельные потоки: производительность и подводные камни
- Spliterator: разделение для параллельной обработки
- Бесконечные потоки с iterate и generate