0Pricing
Java Academy · Урок

Потокобезопасность и побочные эффекты

Избегайте общего изменяемого состояния

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

Параллелизм означает совместный доступ

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

import java.util.stream.IntStream;

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

Опасность: изменение общей переменной

Обычный счётчик int, к которому обращаются многие потоки, теряет обновления. Никогда не накапливайте значения в общем изменяемом состоянии внутри forEach.

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger();
        IntStream.rangeClosed(1, 1000).parallel()
            .forEach(n -> counter.incrementAndGet());
        System.out.println(counter.get());
    }
}

Почему обычные коллекции дают сбой

Добавление элементов в небезопасный для потоков ArrayList из параллельного forEach может повредить коллекцию или привести к потере элементов. Решение — вообще не использовать общее изменяемое состояние.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        List<Integer> result = IntStream.rangeClosed(1, 10).parallel()
            .boxed()
            .collect(Collectors.toList());
        System.out.println(result.size());
    }
}

Правильный инструмент: collect

Используйте collect вместо forEach с побочными эффектами. Средства сборщиков безопасно накапливают частичные контейнеры для каждого потока и объединяют их.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        List<Integer> squares = IntStream.rangeClosed(1, 6).parallel()
            .map(n -> n * n)
            .boxed()
            .collect(Collectors.toList());
        System.out.println(squares);
    }
}

Правильный инструмент: reduce

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

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int sum = IntStream.rangeClosed(1, 1000).parallel()
            .reduce(0, Integer::sum);
        System.out.println(sum);
    }
}

Лямбда-выражения без состояния

Лямбда-выражения в потоках должны быть без состояния: их результат зависит только от входных данных, а не от внешних изменяемых переменных или предыдущих элементов.

import java.util.stream.IntStream;

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

Атомарные типы как запасной вариант

Если общий счётчик действительно необходим, используйте атомарные классы, например AtomicLong. Они корректно сериализуют обновления, хотя и увеличивают конкуренцию.

import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        AtomicLong sum = new AtomicLong();
        IntStream.rangeClosed(1, 1000).parallel()
            .forEach(sum::addAndGet);
        System.out.println(sum.get());
    }
}

Параллельные коллекции

Если нужна общая карта для нескольких потоков, безопасным вариантом будет ConcurrentHashMap. Тем не менее по возможности предпочитайте Collectors.groupingByConcurrent.

import java.util.Map;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("a", "bb", "cc", "ddd");
        Map<Integer, List<String>> byLen = words.parallelStream()
            .collect(Collectors.groupingByConcurrent(String::length));
        System.out.println(byLen);
    }
}

Избегайте логики, зависящей от порядка

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

import java.util.stream.IntStream;

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

Чистые функции безопасно распараллеливаются

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

import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> in = List.of("red", "green", "blue");
        String out = in.parallelStream()
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.joining(","));
        System.out.println(out);
    }
}

Отсутствие взаимного вмешательства

Не изменяйте исходную коллекцию, пока поток обходит её. Это приводит к ConcurrentModificationException или неопределённому поведению как при последовательном, так и при параллельном выполнении.

import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Integer> src = List.of(1, 2, 3, 4);
        List<Integer> doubled = src.parallelStream()
            .map(n -> n * 2)
            .collect(Collectors.toList());
        System.out.println(doubled);
    }
}

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

Как рекомендуется накапливать результаты параллельного потока?

Итоги

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

  • Общее изменяемое состояние в параллельных лямбда-выражениях приводит к состояниям гонки.
  • Предпочитайте collect и reduce имеющему побочные эффекты forEach.
  • Сохраняйте лямбда-выражения без состояния, чистыми и не допускающими взаимного вмешательства.
  • Если общего состояния не избежать, используйте атомарные типы или параллельные коллекции, но это крайняя мера.

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

Урок «Потокобезопасность и побочные эффекты» бесплатный?

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

Чему я научусь в уроке «Потокобезопасность и побочные эффекты»?

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

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

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

Сколько времени занимает урок «Потокобезопасность и побочные эффекты»?

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

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

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

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

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