Потокобезопасность и побочные эффекты
Избегайте общего изменяемого состояния
«Потокобезопасность и побочные эффекты» — бесплатный урок 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 — локальная установка не требуется.
Все уроки этого курса
- Создание параллельных потоков
- Когда параллелизм помогает
- Потокобезопасность и побочные эффекты
- Распространённые ошибки