← Назад к вопросам

Почему мы не всегда используем parallelStream?

2.0 Middle🔥 131 комментариев
#Stream API и функциональное программирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему мы не всегда используем parallelStream

parallel streams выглядят привлекательно — они могут использовать несколько потоков и обрабатывать данные быстрее. Но в реальности параллелизм вводит много сложности и издержек, которые часто перевешивают выгоду.

Издержки parallelStream

1. Overhead создания потоков и синхронизации

Создание потока, контекстные переключения, синхронизация — всё это требует времени. Если твоя операция простая и быстрая, overhead может быть больше, чем сама работа:

// Плохой пример
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
    .map(n -> n * 2)
    .reduce(0, Integer::sum);

Здесь overhead создания потоков будет БОЛЬШЕ времени, нужного для обработки 5 чисел.

2. Проблемы с синхронизацией

Когда несколько потоков работают одновременно, они могут конфликтовать при доступе к разделяемым ресурсам:

// Опасно!
List<Integer> result = new ArrayList<>();nList<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

numbers.parallelStream()
    .forEach(n -> result.add(n * 2)); // RACE CONDITION!
    // Два потока могут одновременно писать в ArrayList

ArrayList не потокобезопасна. Без синхронизации можно потерять данные.

3. Непредсказуемый порядок обработки

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

Когда parallelStream ПОЛЕЗЕН

parallel streams имеют смысл ТОЛЬКО когда:

  1. Большой объём данных — тысячи и миллионы элементов
  2. Дорогая операция — каждый элемент требует много времени на обработку
  3. Данные в памяти — коллекция уже загружена (не стриминг с диска)
  4. Нет конфликтов — операции независимы, нет shared state
// ХОРОШИЙ пример parallelStream
List<Image> images = loadMillionImages(); // 1 млн картинок

List<Image> processed = images.parallelStream()
    .map(img -> applyComplexFilter(img)) // Сложная операция
    .collect(Collectors.toList());

Здесь:

  • Миллион элементов (стоит overhead)
  • Каждый элемент обрабатывается долго (150ms+ per image)
  • Операция независима (нет race conditions)
  • Результат улучшится на 4x с 4-core процессором

Бенчмарк

Операция: map(n -> n * 2) на 1000 элементов

sequential:  0.5ms
parallel:    2.0ms (overhead больше, чем выигрыш)

Операция: map(сложная_обработка) на 1M элементов, ~100ms per element

sequential:  100,000ms (1,666 сек на 1М элементов)
parallel:    25,000ms (416 сек на 4 ядрах) — ВЫИГРЫШ 4x!

Особенности parallelStream

Использует общий ForkJoinPool

Все parallelStreams используют один общий пул потоков (ForkJoinPool.commonPool()). Если твой код параллельно обрабатывает несколько потоков, они конкурируют за ресурсы:

// Два parallelStream в разных потоках конкурируют за один пул
Thread t1 = new Thread(() -> {
    list1.parallelStream().forEach(this::process);
});

Thread t2 = new Thread(() -> {
    list2.parallelStream().forEach(this::process);
});

t1.start();
t2.start();

Сложно отладить

Multi-threading ошибки (race conditions, deadlocks) очень сложно найти и воспроизвести.

Как правильно использовать parallelStream

1. Сначала напиши sequential, потом profile

// Шаг 1: Напиши обычный stream
List<Result> results = data.stream()
    .map(this::process)
    .collect(Collectors.toList());

// Шаг 2: Измерь время
long start = System.currentTimeMillis();
List<Result> results = data.stream()
    .map(this::process)
    .collect(Collectors.toList());
long duration = System.currentTimeMillis() - start;
System.out.println("Sequential: " + duration + "ms");

// Шаг 3: Если медленно, только тогда пробуй parallel
if (duration > 1000) { // Медленнее 1 сек?
    results = data.parallelStream()
        .map(this::process)
        .collect(Collectors.toList());
}

2. Используй правильный Collector

Не все collectors хорошо работают с parallel:

// Плохо для parallel
List<Integer> result = stream.parallel()
    .collect(Collectors.toList()); // ArrayList не параллельна

// Хорошо для parallel
List<Integer> result = stream.parallel()
    .collect(Collectors.toCollection(CopyOnWriteArrayList::new));

3. Тестируй оба варианта

long sequentialTime = benchmarkSequential(data);
long parallelTime = benchmarkParallel(data);

if (parallelTime < sequentialTime) {
    useParallel = true; // Да, параллель быстрее
} else {
    useParallel = false; // Нет, последовательно быстрее
}

Рекомендации

СценарийРекомендация
< 1000 элементовsequential
1000-10000 элементовsequential
10000+ элементов, быстрая операцияsequential
10000+ элементов, дорогая операцияparallel
I/O операции (сеть, диск)parallel или CompletableFuture
Real-time системыsequential (в целях предсказуемости)

Альтернативы parallelStream

Часто mejor использовать явный многопоточной код:

// Вместо parallelStream, используй ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Result>> futures = new ArrayList<>();

for (Item item : data) {
    futures.add(executor.submit(() -> process(item)));
}

List<Result> results = futures.stream()
    .map(f -> {
        try {
            return f.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    })
    .collect(Collectors.toList());

Этот подход дает тебе больше контроля и лучше для читаемости.

Вывод

Не используй parallelStream просто так. Используй его только когда:

  • Сотни тысяч элементов
  • Каждый элемент требует дорогую обработку
  • Нет конфликтов с разделяемым состоянием
  • Бенчмарк показал реальное улучшение

В 90% случаев sequential stream достаточно быстрый, проще и безопаснее. Профилируй перед оптимизацией!

Почему мы не всегда используем parallelStream? | PrepBro