Bezpieczeństwo wątkowe i efekty uboczne
Unikanie współdzielonego, zmiennego stanu
Bezpieczeństwo wątkowe i efekty uboczne to bezpłatna lekcja Java Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Java Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Java Academy zawiera 4 lekcji w sumie.
Równoległość oznacza współbieżny dostęp
W strumieniu równoległym wiele wątków wykonuje lambdy jednocześnie. Każdy współdzielony stan zmienny, którego dotykają, staje się źródłem wyścigu.
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);
}
}Zagrożenie: modyfikowanie współdzielonej zmiennej
Zwykły licznik typu int używany przez wiele wątków gubi aktualizacje. Nigdy nie należy gromadzić wyników we współdzielonym stanie zmiennym wewnątrz 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());
}
}Dlaczego zwykłe kolekcje zawodzą
Dodawanie elementów do niebezpiecznej wątkowo klasy ArrayList z równoległego forEach może ją uszkodzić lub spowodować utratę elementów. Rozwiązaniem nie jest współdzielenie i modyfikowanie stanu, lecz całkowite unikanie takiego podejścia.
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());
}
}Właściwe narzędzie: collect
Należy używać collect zamiast powodującego skutki uboczne forEach. Mechanizm kolektorów bezpiecznie gromadzi częściowe kontenery w poszczególnych wątkach i łączy je.
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);
}
}Właściwe narzędzie: reduce
reduce jest z natury bezpieczny wątkowo, gdy operacja jest asocjacyjna i bezstanowa, ponieważ łączy częściowe wyniki bez współdzielonego modyfikowania stanu.
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);
}
}Bezstanowe lambdy
Lambdy w strumieniach powinny być bezstanowe: ich wynik zależy wyłącznie od danych wejściowych, a nie od zewnętrznych zmiennych zmiennych ani wcześniejszych elementów.
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);
}
}Typy atomowe jako rozwiązanie awaryjne
Jeśli współdzielenie licznika jest naprawdę konieczne, należy użyć klas atomowych, takich jak AtomicLong. Poprawnie serializują one aktualizacje, choć powodują rywalizację między wątkami.
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());
}
}Kolekcje współbieżne
Gdy potrzebna jest współdzielona mapa używana przez wiele wątków, bezpiecznym rozwiązaniem jest ConcurrentHashMap. Mimo to, jeśli to możliwe, należy preferować 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);
}
}Należy unikać logiki zależnej od kolejności
Nie należy polegać na kolejności napotkania elementów wewnątrz lambd równoległych. Operacje zakładające przetwarzanie sekwencyjne dają niepoprawne wyniki w trybie równoległym.
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);
}
}Funkcje czyste można bezpiecznie wykonywać równolegle
Najważniejsza zasada: należy pisać operacje czyste, bezstanowe i nieingerujące. Wtedy strumień może działać sekwencyjnie lub równolegle, zawsze zwracając identyczne i poprawne wyniki.
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);
}
}Brak ingerencji
Nie należy modyfikować kolekcji źródłowej podczas działania strumienia przechodzącego po jej elementach. Powoduje to ConcurrentModificationException lub niezdefiniowane działanie, zarówno sekwencyjne, jak i równoległe.
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);
}
}Szybkie sprawdzenie
Jaki jest zalecany sposób gromadzenia wyników ze strumienia równoległego?
Podsumowanie
Zapewniono poprawność strumieni równoległych:
- Współdzielony stan zmienny w równoległych lambdach powoduje wyścigi.
- Należy preferować
collectireducezamiast powodującego skutki uboczneforEach. - Lambdy powinny być bezstanowe, czyste i nieingerujące.
- Jeśli współdzielenie jest konieczne, należy użyć typów atomowych lub kolekcji współbieżnych, ale powinno to być rozwiązanie ostateczne.
Często zadawane pytania
Czy lekcja „Bezpieczeństwo wątkowe i efekty uboczne” jest bezpłatna?
Tak — pełny tekst „Bezpieczeństwo wątkowe i efekty uboczne” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Java Academy, przejdź na CoddyKit PRO. Kurs Java Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „Bezpieczeństwo wątkowe i efekty uboczne”?
Unikanie współdzielonego, zmiennego stanu Ćwiczysz Java Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Java Academy?
Nie wymagamy żadnego doświadczenia. Java Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.
Ile czasu zajmuje lekcja „Bezpieczeństwo wątkowe i efekty uboczne”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Java Academy?
Tak. Każda lekcja Java Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Tworzenie strumieni równoległych
- Kiedy równoległość pomaga
- Bezpieczeństwo wątkowe i efekty uboczne
- Typowe pułapki