0Pricing
Java Academy · Lekcja

Strumienie równoległe: wydajność i pułapki

Proszę włączać strumienie równoległe, poznać wspólną pulę wątków i unikać błędów związanych ze współdzielonym zmiennym stanem.

Strumienie równoległe: wydajność i pułapki to bezpłatna lekcja Java Academy na CoddyKit. To lekcja 2 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.

Włączanie strumieni równoległych

Należy wywołać .parallel() na dowolnym strumieniu lub użyć Collection.parallelStream(). Operacje strumienia są wykonywane we wspólnej puli ForkJoinPool (domyślnie: liczba wątków równa liczbie rdzeni CPU pomniejszonej o 1).

long count = list.parallelStream()
    .filter(n -> n % 2 == 0)
    .count();
System.out.println(count);

Kiedy strumienie równoległe się opłacają

Strumienie równoległe warto stosować, gdy: zbiór danych jest duży (100 000 lub więcej elementów), operacja na każdym elemencie jest kosztowna obliczeniowo, a potok jest bezstanowy i niezależny od kolejności.

// Good candidate: CPU-heavy computation on large dataset
long sum = LongStream.rangeClosed(1, 10_000_000)
    .parallel()
    .filter(n -> isPrime(n))
    .sum();
System.out.println(sum);

Kiedy NIE używać strumieni równoległych

Należy unikać strumieni równoległych w przypadku: małych kolekcji, operacji związanych z wejściem i wyjściem (blokują one wątki ForkJoinPool), operacji stanowych (sorting, distinct) lub gdy kolejność ma znaczenie i jej przywrócenie jest kosztowne.

// Bad: I/O bound — blocking ForkJoinPool threads starves other tasks
List<String> result = urls.parallelStream()
    .map(url -> httpGet(url)) // BLOCKS the common pool
    .collect(Collectors.toList());

Błąd związany ze współdzielonym stanem modyfikowalnym

Strumienie równoległe wykonują operacje jednocześnie w wielu wątkach. Modyfikowanie współdzielonej, modyfikowalnej kolekcji powoduje wyścigi danych i nieprawidłowe wyniki.

// RACE CONDITION — never do this:
List<Integer> results = new ArrayList<>(); // not thread-safe
numbers.parallelStream().forEach(n -> results.add(n)); // corrupts list!
// Fix:
List<Integer> safe = numbers.parallelStream().collect(Collectors.toList());

Operacje stanowe: sorted i distinct

sorted() i distinct() wymagają obejrzenia wszystkich elementów przed wygenerowaniem wyniku, co ogranicza równoległość i często sprawia, że wykonanie równoległe jest wolniejsze niż sekwencyjne.

// sorted() forces collect-all, then sort — parallel overhead usually not worth it:
list.parallelStream().sorted().collect(Collectors.toList());

Operacje zależne od kolejności

findFirst() i forEachOrdered() zachowują kolejność napotkania w strumieniach równoległych, co zwiększa koszt synchronizacji. Gdy kolejność nie ma znaczenia, należy użyć findAny() lub forEach().

// Faster in parallel (order-insensitive):
Optional<Integer> any = list.parallelStream().filter(n -> n > 10).findAny();
// Slower in parallel (must preserve order):
Optional<Integer> first = list.parallelStream().filter(n -> n > 10).findFirst();

Wybór rozmiaru puli wątków

Wspólna pula ForkJoinPool używa Runtime.getRuntime().availableProcessors() - 1 wątków. Strumienie równoległe można uruchamiać w niestandardowej puli, wykonując je wewnątrz wywołania ForkJoinPool.invoke().

ForkJoinPool custom = new ForkJoinPool(8);
long result = custom.submit(
    () -> LongStream.rangeClosed(1, 1_000_000).parallel().sum()
).get();

Testowanie wydajności równoległej i sekwencyjnej

Zawsze należy mierzyć wydajność za pomocą JMH, używając realistycznych rozmiarów danych. Narzut operacji równoległych (dzielenie, koordynacja wątków, łączenie wyników) jest uzasadniony tylko wtedy, gdy czas obliczeń wielokrotnie przewyższa ten narzut.

@Benchmark
public long sequential() { return LongStream.rangeClosed(1,1_000_000).sum(); }
@Benchmark
public long parallel()   { return LongStream.rangeClosed(1,1_000_000).parallel().sum(); }

Operacje redukcji wykonywane równolegle

reduce() i collect() są zaprojektowane tak, aby działać poprawnie równolegle, gdy operacje są łączne, a wartość tożsamościowa jest prawidłowa.

// Associative reduce — safe in parallel:
int sum = list.parallelStream().reduce(0, Integer::sum);
// Non-associative: subtraction — NOT safe in parallel:
int bad = list.parallelStream().reduce(0, (a, b) -> a - b); // wrong result!

Znaczenie możliwości dzielenia

Strumienie równoległe dzielą źródło danych za pomocą Spliterator. ArrayList i tablice dzielą się w czasie O(1); LinkedList i HashSet dzielą się nieefektywnie, co zmniejsza wydajność przetwarzania równoległego.

Podsumowanie: lista kontrolna strumieni równoległych

Przed użyciem przetwarzania równoległego należy upewnić się, że: (1) zbiór danych jest duży, (2) operacje obciążają CPU, (3) nie ma współdzielonego stanu modyfikowalnego, (4) kolejność nie ma znaczenia, (5) źródło można dzielić (tablica lub ArrayList). W razie wątpliwości należy zmierzyć wydajność.

Szybki test

Co się stanie po dodaniu elementu do kolekcji niebezpiecznej wątkowo w metodzie forEach strumienia równoległego?

Powtórzenie

Strumienie równoległe korzystają z ForkJoinPool. Są skuteczne w przypadku dużych zbiorów danych oraz bezstanowych potoków obciążających CPU i niezależnych od kolejności. Nie należy nigdy modyfikować współdzielonego stanu. Przed zastosowaniem przetwarzania równoległego należy zmierzyć wydajność — w przypadku małych zbiorów danych jest ono często wolniejsze.

Często zadawane pytania

Czy lekcja „Strumienie równoległe: wydajność i pułapki” jest bezpłatna?

Tak — pełny tekst „Strumienie równoległe: wydajność i pułapki” 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 „Strumienie równoległe: wydajność i pułapki”?

Proszę włączać strumienie równoległe, poznać wspólną pulę wątków i unikać błędów związanych ze współdzielonym zmiennym stanem. Ć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 2 z 4.

Ile czasu zajmuje lekcja „Strumienie równoległe: wydajność i pułapki”?

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

  1. flatMap dla zagnieżdżonych kolekcji
  2. Strumienie równoległe: wydajność i pułapki
  3. Spliterator: dzielenie na potrzeby równoległości
  4. Nieskończone strumienie za pomocą iterate i generate
← Powrót do Java Academy