0Pricing
Java Academy · Урок

Распространённые ошибки

Порядок, упаковка и общий пул

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

Параллелизм не даёт бесплатного ускорения

Добавление parallel() может замедлить код, а не ускорить его. В этом уроке рассматриваются классические ловушки, чтобы вы могли их избежать.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        long sum = IntStream.rangeClosed(1, 1_000_000).parallel()
            .asLongStream().sum();
        System.out.println(sum);
    }
}

Ловушка: расходы на сохранение порядка

Операции, сохраняющие порядок, такие как forEachOrdered или limit, требуют координации между потоками и уменьшают выигрыш от параллелизма.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        IntStream.rangeClosed(1, 5).parallel()
            .forEachOrdered(System.out::println);
    }
}

unordered может помочь

Если порядок не важен, вызов unordered() позволяет среде выполнения пропустить работу по сохранению порядка и свободнее распараллелить обработку.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        long count = IntStream.rangeClosed(1, 1_000_000).parallel()
            .unordered()
            .filter(n -> n % 5 == 0)
            .count();
        System.out.println(count);
    }
}

Ловушка: неявное упаковывание

Использование Stream<Integer> в параллельном режиме увеличивает расходы на упаковывание в разных потоках. Для параллельной обработки чисел предпочитайте потоки примитивных значений.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        long sum = IntStream.rangeClosed(1, 1_000_000).parallel().asLongStream().sum();
        System.out.println(sum);
    }
}

Ловушка: общий пул

Все параллельные потоки используют один ForkJoinPool.commonPool(). Длительная параллельная задача может лишить ресурсов другие задачи в той же JVM, включая другие части вашего приложения.

import java.util.concurrent.ForkJoinPool;

public class Main {
    public static void main(String[] args) {
        System.out.println(ForkJoinPool.commonPool().getParallelism());
    }
}

Изоляция работы в пользовательском пуле

Чтобы не выполнять тяжёлую работу в общем пуле, передайте параллельный поток в собственный ForkJoinPool.

import java.util.concurrent.ForkJoinPool;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) throws Exception {
        ForkJoinPool pool = new ForkJoinPool(2);
        long sum = pool.submit(() ->
            IntStream.rangeClosed(1, 1_000_000).parallel().asLongStream().sum()
        ).get();
        pool.shutdown();
        System.out.println(sum);
    }
}

Ловушка: источники, плохо допускающие разделение

LinkedList, Stream.iterate и BufferedReader.lines плохо разделяются. Их распараллеливание часто даёт небольшую пользу или даже ухудшает производительность.

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        long n = Stream.iterate(1, x -> x + 1)
            .limit(100_000)
            .parallel()
            .filter(x -> x % 2 == 0)
            .count();
        System.out.println(n);
    }
}

Ловушка: лямбда-выражения с состоянием

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

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int sum = IntStream.rangeClosed(1, 100).parallel()
            .map(n -> n + 1)
            .sum();
        System.out.println(sum);
    }
}

Ловушка: findFirst и findAny

findFirst должен соблюдать порядок, что увеличивает расходы в параллельном режиме. Если подходит любой совпавший элемент, используйте findAny.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int found = IntStream.rangeClosed(1, 1_000_000).parallel()
            .filter(n -> n % 99991 == 0)
            .findAny()
            .getAsInt();
        System.out.println(found);
    }
}

Ловушка: дешёвые операции на небольших данных

Для крошечных коллекций или тривиальной работы расходы на разделение и объединение значительно превышают любую пользу. Оставайтесь в последовательном режиме.

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> small = List.of(1, 2, 3, 4);
        int sum = small.stream().mapToInt(Integer::intValue).sum();
        System.out.println(sum);
    }
}

Ловушка: дорогое объединение в сборщиках

Сборщики с затратным этапом объединения, например создающие большие отсортированные структуры, могут свести на нет выигрыш от параллелизма. Простые операции накопления объединяются дёшево.

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 -> n)
            .sum();
        System.out.println(total);
    }
}

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

Почему findFirst может быть медленнее findAny в параллельном потоке?

Итоги

Вы узнали о распространённых ловушках параллельной обработки:

  • Сохранение порядка (forEachOrdered, limit, findFirst) требует дополнительных затрат на координацию; по возможности используйте unordered или findAny.
  • Упаковывание увеличивает расходы; предпочитайте потоки примитивных значений.
  • Общий пул может быть лишён ресурсов; изолируйте тяжёлую работу в пользовательском ForkJoinPool.
  • Источники, плохо допускающие разделение, небольшие объёмы данных и дорогие операции объединения снижают эффективность параллелизма.

Часто задаваемые вопросы

Урок «Распространённые ошибки» бесплатный?

Да — полный текст урока «Распространённые ошибки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.

Чему я научусь в уроке «Распространённые ошибки»?

Порядок, упаковка и общий пул Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Java Academy?

Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Распространённые ошибки»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Java Academy?

Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

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

  1. Создание параллельных потоков
  2. Когда параллелизм помогает
  3. Потокобезопасность и побочные эффекты
  4. Распространённые ошибки
← Назад к Java Academy