Java Academy · Oppitunti

Rinnakkaiset streamit: suorituskyky ja sudenkuopat

Ota rinnakkaiset streamit käyttöön, tutustu yhteiseen säiepooliin ja vältä jaettuun muuntuvaan tilaan liittyvät virheet.

Oppitunti 2/413 vaihetta

Rinnakkaiset streamit: suorituskyky ja sudenkuopat on ilmainen Java Academy-oppitunti CoddyKitissä. Tämä on oppitunti 2/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu Java Academy-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Java Academy-kurssilla on yhteensä 4 oppituntia.

Rinnakkaisten streamien käyttöönotto

Kutsukaa .parallel()-metodia missä tahansa streamissa tai käyttäkää metodia Collection.parallelStream(). Stream-operaatiot suoritetaan yhteisessä ForkJoinPool-poolissa (oletus: suorittimen ytimet − 1 säiettä).

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

Milloin rinnakkaisuudesta on hyötyä

Rinnakkaiset streamit ovat hyödyllisiä, kun tietojoukko on suuri (vähintään 100 000 alkiota), kunkin alkion käsittely on laskennallisesti raskasta ja käsittelyputki on tilaton eikä riipu järjestyksestä.

// 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);

Milloin rinnakkaisia streameja EI pidä käyttää

Välttäkää rinnakkaisia streameja seuraavissa tapauksissa: pienet kokoelmat, I/O-sidonnaiset operaatiot (ne estävät ForkJoinPoolin säikeitä), tilaa säilyttävät operaatiot (sorting, distinct) tai tilanteet, joissa järjestyksellä on merkitystä ja sen palauttaminen on kallista.

// 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());

Virhe jaetussa muuttuvassa tilassa

Rinnakkaiset streamit suorittavat operaatioita samanaikaisesti useissa säikeissä. Jaetun muuttuvan kokoelman muokkaaminen aiheuttaa datakilpailuja ja virheellisiä tuloksia.

// 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());

Tilaa säilyttävät operaatiot: sorted ja distinct

sorted() ja distinct() edellyttävät kaikkien alkioiden näkemistä ennen tulosteen tuottamista, mikä rajoittaa rinnakkaisuutta ja tekee rinnakkaisesta käsittelystä usein peräkkäistä hitaampaa.

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

Järjestyksestä riippuvat operaatiot

findFirst() ja forEachOrdered() säilyttävät kohtaamisjärjestyksen rinnakkaisissa streameissa, mikä lisää synkronoinnin kustannuksia. Käyttäkää findAny()- tai forEach()-metodia, kun järjestyksellä ei ole merkitystä.

// 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();

Säiepoolin koon valitseminen

Yhteinen ForkJoinPool käyttää Runtime.getRuntime().availableProcessors() - 1 säiettä. Suorittakaa rinnakkaiset streamit mukautetussa poolissa lähettämällä ne ForkJoinPool.invoke()-kutsun sisällä.

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

Rinnakkaisen ja peräkkäisen käsittelyn vertailumittaus

Mitatkaa aina JMH:lla realistisilla tietojoukoilla. Rinnakkaisuuden yleiskustannukset (jakaminen, säikeiden koordinointi ja yhdistäminen) ovat perusteltuja vain, kun laskennan kesto ylittää yleiskustannukset moninkertaisesti.

@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(); }

Reduktio-operaatiot rinnakkaisessa käsittelyssä

reduce() ja collect() on suunniteltu toimimaan oikein rinnakkaisesti, kun operaatiot ovat assosiatiivisia ja identiteettiarvo on oikea.

// 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!

Jaettavuudella on merkitystä

Rinnakkaiset streamit jakavat tietolähteen Spliterator-olion avulla. ArrayList ja taulukot jakautuvat ajassa O(1), kun taas LinkedList ja HashSet jakautuvat heikosti, mikä vähentää rinnakkaisuuden tehokkuutta.

Yhteenveto: rinnakkaisten streamien tarkistuslista

Ennen rinnakkaisuuden käyttämistä tarkistakaa: (1) suuri tietojoukko, (2) suoritinsidonnaiset operaatiot, (3) ei jaettua muuttuvaa tilaa, (4) riippumattomuus järjestyksestä, (5) jaettava lähde (taulukko/ArrayList). Jos ette ole varmoja, tehkää vertailumittaus.

Pikatarkistus

Mitä tapahtuu, kun lisäätte säieturvallisuutta vailla olevaan kokoelmaan rinnakkaisen streamin forEach-käsittelyssä?

Kertaus

Rinnakkaiset streamit käyttävät ForkJoinPoolia. Ne sopivat tehokkaasti suuriin, suoritinsidonnaisiin ja järjestyksestä riippumattomiin tilattomiin käsittelyputkiin. Älkää koskaan muuttako jaettua tilaa. Tehkää vertailumittaus ennen käyttöönottoa — rinnakkaisuus on usein pientä tietomäärää käsiteltäessä hitaampaa.

Aloita maksutta

Opi Java tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
104
Oppitunnit
374

Usein kysytyt kysymykset

Onko oppitunti ”Rinnakkaiset streamit: suorituskyky ja sudenkuopat” ilmainen?

Kyllä – oppitunnin ”Rinnakkaiset streamit: suorituskyky ja sudenkuopat” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko Java Academy-kurssin, päivitä CoddyKit PROhon. Java Academy-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Rinnakkaiset streamit: suorituskyky ja sudenkuopat”?

Ota rinnakkaiset streamit käyttöön, tutustu yhteiseen säiepooliin ja vältä jaettuun muuntuvaan tilaan liittyvät virheet. Harjoittelet Java Academy-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni Java Academy-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin Java Academy-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 2/4.

Kuinka kauan ”Rinnakkaiset streamit: suorituskyky ja sudenkuopat”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä Java Academy-oppitunnilla?

Kyllä. Jokainen Java Academy-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. flatMap sisäkkäisille kokoelmille
  2. Rinnakkaiset streamit: suorituskyky ja sudenkuopat
  3. Spliterator: jakaminen rinnakkaisuutta varten
  4. Infinite Streams with iterate and generate
← Takaisin: Java Academy