[x^2 for x in lst]

Nyheter i Java 8 - del 5 - parallella strömmar

2014-09-09

Detta är den femte delen i miniserien om nyheterna i Java 8. De tidigare delarna har bland annat handlat om de nya sakerna som finns kan finnas i interface samt om strömmar och lambdauttryck och vad man kan göra med dem.

Den här delen skall handla om hur man kan parallellisera exekveringen av vissa typer av program genom att använda parallella strömmar.

Parallella strömmar

Naiv metod för att bestämma om ett tal är ett primtal eller inte

Som ett exempel på hur man kan parallellisera exekveringen av vissa operationer som utförs på strömmar skall vi kolla på hur man kan hitta alla primtal mellan 1 och 10 miljoner. Vi börjar med att skriva en liten, mycket naiv, metod för att bestämma om ett tal är ett primtal eller inte. Notera som sagt att den är mycket naiv. Om någon skulle anklaga mig för att vara upphovsmannen till den skulle jag förneka detta kategoriskt ;) Men den duger bra för att exemplifiera konceptet med parallell exekvering av strömmar.

// Most inefficient algorithm.
private static boolean isPrime(int n) {
  if (n == 2) {
    return true;
  } else if (n <= 1 || n % 2 == 0) {
    return false;
  }

  for (int possibleDivisor = 3; possibleDivisor <= Math.ceil(Math.sqrt(n)); possibleDivisor += 2) {
    if (n % possibleDivisor == 0) {
      return false;
    }
  }

  return true;
}

Beräkna primtal sekventiellt

Vi kan nu använda den statiska metoden rangeClosed() i klassen IntStream (som vi ju pratade om i del 3) för att generera en ström som innehåller en uppsättning med heltal. Vi är intresserade av heltalen mellan 1 och 10 miljoner, så för att generera en ström som innehåller dessa tal kan vi göra så här:

IntStream.rangeClosed(1, 10000000)

För att extrahera ut de av talen som är primtal kan vi sen applicera metoden filter() på strömmern och använda en metodreferens till vår naiva primtalkollarmetod (som finns i klassen MainClass):

IntStream.rangeClosed(1, 10000000).filter(MainClass::isPrime)

Vill vi sedan samla upp primtalen för vidare bearbetning kan vi använda collect(). Vi passar även på att lägga en utskrft på hur lång tid exekveringen tar, så vi har något att jämföra med senare. Med dessa ändringar ser vårt lilla program ut så här:

long startTime = System.currentTimeMillis();
List<Integer> primes = IntStream.rangeClosed(1, 10000000).filter(MainClass::isPrime).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
primes.stream().forEach(System.out::println);  
long endTime = System.currentTimeMillis();
System.out.println("Execution took: " + (endTime - startTime) + "ms"); 

I exemplet så har "den första" varianten av collect() använts. Detta eftersom det är den enda som finns i klassen IntStream.

Om man kör ovanstående program på min, väldigt långsamma, "bussdator" ser det ut så här:

lars@larsLilla:~/text/blogg/140903$ java -classpath . MainClass
Execution took: 53197ms
lars@larsLilla:~/text/blogg/140903$ 

Beräkna primtal parallellt

Hur gör vi nu om vi vill parallellisera exekveringen av primtalssökningen? Ja, först kan vi konstatera att metoden som kollar om ett tal är ett primtal eller inte är tillståndslös. Det är ju bra, då har den ju inga konstiga sidoeffekter som vi behöver ta hänsyn till. Dock modifierar ju metoden filter() strömmen. Det är ju hela poängen med den metoden. Så hur hanterar vi detta om vi nu vill parallellisera exekveringen?

Tittar man på API-dokumentationen av IntStream så ser man att den har en metod parallel() som det står "Returns an equivalent stream that is parallel.". Lysande! Vi provar:

long startTime = System.currentTimeMillis();
List<Integer> primes = IntStream.rangeClosed(1, 10000000).parallel().filter(MainClass::isPrime).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
long endTime = System.currentTimeMillis();
System.out.println("Execution took: " + (endTime - startTime) + "ms"); 
...
Execution took: 44177ms

Det var allt vi behövde göra för att parallellisera exekveringen av primtalssökningen. Inte en så stor ändring, eller hur?

Nu var i och för sig minskningen av exekveringstid inte gigantisk. Den blev ungefär 17% kortare, men detta kan nog till viss del bero på att den lilla, klena dator jag använder när jag sitter på bussen och kodar och skriver (som jag gör nu) bara har två kärnor. Kör man exempelprogrammet på den dator jag använder på jobbet blir tiderna istället 3,7s respektive 1,2s. I detta fall går alltså det parallella programmet tre gånger så snabbt som det sekventiella. Inte dåligt!

Man kan också konstatera att jag skulle behöva en ny bussdator.

Andra parallella strömmar

Så vad finns det då för stöd i API:t för att parallellisera exekveringen av operationer på strömmar? Vi har ju redan sett metoden parallel() i IntStream. Egentligen är det så att parallel() finns i interfacet BaseStream som förutom IntStream också implementeras av DoubleStream och LongStream. Men om det inte är en numerisk ström man vill använda då? Det visar sig att klassen Collection förutom att ha en metod stream() också har en metod som heter parallelStream(). Super! Istället för att använda stream() och få en sekventiell ström kan vi använda parallelStream() och få en parallell ström. Vi behöver inte skapa några egna trådar eller trådpoler, bara anropa parallelStream().

Varför inte alltid använda parallella strömmar? - sidoeffekter

Det låter ju nästan alltför bra att vi bara genom att anropa BaseStream.parallel() eller Collection.parallelStream() kan parallellisera alla program som använder strömmar. Finns det inga hakar då? Såklart det gör. Som med all programmering där parallellism är inblandat så får man hålla tungan rätt i mun. Har man lambdauttryck som uppdaterar något gemensamt tillstånd så är det ingen bra ide att använda parallella strömmar. Man får fundera igenom om operationerna man vi ha utförda verkligen går att exekvera parallellt innan man använder BaseStream.parallel() eller Collection.parallelStream().

Som ett exempel på när det inte blir ett korrekt resultat om parallella strömmar används skall vi skriva ett litet program som givet en lista med heltal ökar varje heltal i listan med en ökande offset (1, 2, ...).

Vi skapar en metod testAddToStream(IntStream stream) som tar en IntStream in som parameter. Den använder sedan map för att addera allt större tal till elementen i strömmen. Vilket tal som skall adderas hålls i en instansvariabel. Detta är ju jättekorkat egentligen eftersom det hade fungerat lika bra med en lokal variabel. Dessutom hade det blivit ett korrekt resultat även för parallella strömmar om så hade skett, men vi gör så bara för att visa ett exempel på hur det kan gå snett med parallella strömmar. Metoden ser i alla fall ut så här:

public class MutableTest
{
  private int offset;
...
  private Set<Integer> testAddToStream(IntStream stream) {
    offset = 0;
    return stream.map(i -> i + offset++).collect(HashSet::new, HashSet::add, HashSet::addAll);
  }
}

Fyller vi sedan på klassen med en metod som anropar testAddToStream() med en sekventiell ström och en metod som anropar den med en parallell ström och skriver ut hur många unika heltal som returneras ser klassen ut så här:

public class MutableTest
{
  private int offset;

  public static void main(String[] args) {
    MutableTest instanceOne = new MutableTest();
    instanceOne.testSequential();

    MutableTest instanceTwo = new MutableTest();
    instanceTwo.testParallel();
  }

  private void testSequential() {
    Set<Integer> uniqueNumbers = testAddToStream(IntStream.rangeClosed(1, 100));
    System.out.println("Number of unique numbers using sequential stream: " + uniqueNumbers.size()); 
  }

  private void testParallel() {
    Set<Integer> uniqueNumbers = testAddToStream(IntStream.rangeClosed(1, 100).parallel());
    System.out.println("Number of unique numbers using parallel stream: " + uniqueNumbers.size()); 
  }

  private Set<Integer> testAddToStream(IntStream stream) {
    offset = 0;
    return stream.map(i -> i + offset++).collect(HashSet::new, HashSet::add, HashSet::addAll);
  }
}
Eftersom vi skapar en ström med 100 heltal förväntar vi oss att utskrifterna skall indikera att testSequential() och testParallel() rapporterar att 100 heltal returnerats från testAddToStream(). Blir det så då?

Nej, inte direkt. Kör man ovanstående klass fås, på min maskin, följande resultat:

ars@larsLilla:~/text/blogg/140903$ java -classpath . MutableTest 
Number of unique numbers using sequential stream: 100
Number of unique numbers using parallel stream: 74
lars@larsLilla:~/text/blogg/140903$ 

Resultatet från olika körningar ger lite olika resultat av testet med den parallella strömmen. 74 och 75 verkar vara de vanligaste resultaten. Inte en enda gång har det blivit 100 i alla fall. Orsaken är givetvis att flera trådar använder samma värde av offset, vilket gör att duplikat uppträder ni resultatströmmen. När denna görs om till ett Set elimineras resultaten och antalet unika heltal i minskar. Inga konstigheter, men ett exempel på hur man inte skall använda parallella strömmar.

Varför inte alltid använda parallella strömmar? - gemensam trådpool

Googlar man lite på parallellism i java 8 så hittar man snabbt flera texter som varnar lite för användandet av parallella strömmar. Se till exempel [3] och [4] nedan. Orsaken till varför de råder en att tänka över användandet av parallella strömmar är att de är implementerade med hjälp av JVM:ens fork-join-pool (fjp). Var är det som är farligt med det då? Jo, tydligen delas JVM:ens fjp mellan alla trådar i hela JVM:en. Till saken hör också att fjp:n är implementerad på så sätt att om en en trådarna i poolen hänger på ett blockerande anrop kommer poolen inte att fyllas på med en nystartad tråd, utan antalet tillgängliga trådar i poolen kommer att minska med en. Sammantaget innebär detta att om flera delar av en applikation använder fjp:n, t.ex. genom att använda parallella strömmar, kommer de att kunna störa varandra om pooltrådarna används till blockerande anrop. Om man använder parallella strömmar har man har alltså inte någon kontroll på hur många trådar, om ens någon, som kommer att exekvera ens lambdauttryck. Allt hänger på hur resten av applikatonen använder JVM:ens fjp.

Skriver man en singeltrådad applikation, eller en multitrådad applikation där endast en tråd använder fjp:n, är detta givetvis inte något problem. Det kan dock vara bra att veta om hur kopplingen mellan parallella strömmar och fjp:n ser ut så man kan undvika att använda dem då sådana här krockar skulle kunna inträffa.

Avslutning del 5

Den här gången har vi gått igenom hur parallella strömmar kan användas. Och när de inte bör användas. Jag tycker att de är ett alldeles utmärkt verktyg för vissa sorters problem. I vissa Project Euler-uppgifter hade de snabbat upp mina lösningar avsevärt (det vill säga om jag skrivit dem i Java och inte exekverat dem på min bussdator). Det är nästan så jag vill skriva om en del av mina lösningar till att använda parallella strömmar för att se hur mycket snabbare jag kan få dem att exekvera.

I nästa del i serien skall vi kolla lite på hur man kan parallellisera vissa operationer på arrayer med metoder som är nytillagda i Java 8.

Tidigare delar i serien

Del 1: Interface
Del 2: Streams och lambda expressions
Del 3: Metoder i Stream, metod- och konstruktorreferenser
Del 4: Metoder klassen Collectors

Referenser

[1] Subramaniam V, Functional Programming in Java, första utgåvan, The Pragmatic Programmers, 2014.
[2] The Java Tutorial - Parallelism
[3] Think twice before using Java 8 parallel streams
[4] Java Parallel Streams Are Bad for Your Health!

AAA - Arrange Act Assert

2014-09-19

Idag när jag satt och skrev ett enhetstest erinrade jag mig att jag sett en kollega lägga in tre rubriker i sina enhetstest. Han använde alltid samma rubriker: Arrange, Act och Assert. När jag såg honom skriva ett enhetstest och lägga in de rubrikerna kommer jag ihåg att jag tänkte något i stil med "Det där ser bra ut. Det skall jag komma ihåg att använda!". Sen glömde jag bort det.

Idag kom jag som sagt i alla fall ihåg att jag sett det och att jag hade tyckt att det var bra. Eftersom kollegan gått för dagen beslöt jag mig för att googla lite för att kolla om man kunde hitta något på nätet. Det kan man ju nästan alltid. Om det inte gäller objektdatabasen Versant i alla fall. Det bästa man kan hoppas på då är en länk till någon obskyr artikel på franska. Huga. Tur att man inte behöver jobba med den databasen längre. MongoDB är en så otroligt mycket trevligare bekantskap. Oracle också förresten och MySQL och Postgres. Egentligen är de flesta databaser ganska trevliga att jobba med. Utom Versant.

Åter till ämnet. När jag sökte på nätet efter unit test arrange act assert fick jag en hel del träffar. Det visade sig att Arrange Act Assert var ett enhetstestmönster som beskrevs 2003. Hoppsan. Det hade jag verkligen missat. Det känns som om man verkligen skulle behöva fortbilda sig de där tre timmarna om dagen som Uncle Bob förespråkar i Clean Code för att hinna hänga med som man borde. Fattar bara inte hur man skall hinna med det. I varje fall visade googlingen att Arrange-Act-Assert är ett allmänt spritt mönster.

Vad går då Arrange-Act-Assert-mönstret ut på? Jo, det går ut på att man delar in sina enhetstest i tre delar där koden i varje del är ansvarig för en viss väldefinierad uppgift. Att de olika delarna kallas för Arrange, Act och Assert är kanske inte så svårt att lista ut, men vad skall då ingå i varje del? Jo...

  • Arrange: sätta upp preconditions och skapa data som skall skickas in till koden som skall testas.
  • Act: anropa koden som skall testas.
  • Assert: verifiera att exekveringen gav det förväntade resultatet.

Ett litet exempel kan kanske var på sin plats. Antag att vi har en klass DoStuffer som har en metod int multiplyByTwo(int). Antag att vi vill enhetstesta den här metoden. Hur kan då ett enhetstest som använder Arrange-Act-Assert-mönstret se ut? Kanske så här:

  @Test
  public int testMultiplyByTwo() {
    // Arrange
    int numberToMultiply = 42;

    // Act
    int restult = unitUnderTest.multiplyByTwo(numberToMultiply);

    // Assert
    assertEquals("", numberToMultiply * 2, result);
  }
}

Vi har här delat upp testet i de tre delar som AAA-mönstret föreskriver. I den första delen, Arrange, sätter vi upp indatat som vi skall mata in till metoden vi skall testa. Här kan man även sätta upp mock-objekt och vilka metodanrop man förväntar sig på dem om det nu är så att man använder något mock-ramverk som t.ex. EasyMock eller Mockito.

I den andra delen, Act utför vi själva anropet till det vi vill testa. Det är också det enda vi gör i den här delen.

I den tredje och sista delen, Assert, verifierar vi att anropet till den metod som skulle testat gav det förväntade resultatet. Om mockobjekt används kan vi här verifiera att de förväntade metodanropen gjordes på dem.

Vad är då fördelen med Arrange - Act - Assert-mönstret? Det verkar ju inte vara något speciellt stort och grandiost mönster direkt. Som jag ser det finns det en rad olika fördelar. En fördel är att det är lätt att se var de olika delarna i testet finns när dessa är väl avgränsade. Kommer man in som ny utvecklare och skall ändra eller utöka ett befintligt test är det lätt att hitta var koden man behöver ändra finns. Behöver man ändra indatat görs detta i Arrange-delen, behöver man ändra själva anropet gör detta i Act-delen och behöver man ändra verifieringen av resultatet görs detta i Assert-delen.

En annan fördel är att man inte frestas att göra "flera tester i ett test". Dvs först testa en sak och verifiera den, sedan testa en annan sak och verifiera den i samma test. Eller ännu värre: blanda flera testaanrop och verifieringar med varandra i en enda röra. Använder man Arrange - Act - Assert-mönstret så tvingas man att separera kod för testuppsättning, testanrop och verifikation. Man kan i och för sig fortfarande testa fler än en sak, men det kräver i så fall att man gör fler än ett anrop i Act-delen, och det har vi ju slagit fast ovan att man inte skall göra;)

Notera att exemplet ovan givetvis är ett mycket enkelt exempel och att de testerna man skriver i verkliga livet oftast är bra mycket mer komplicerade och ofta använder diverse mockramverk. Faktum kvarstår dock. Strukturerar man sina enhetstest enligt Arrange - Act - Assert-mönstret kan man öka läsbarheten och minska kostnaden för underhåll av testen. Inte så dumt.

Länkar:

0.0.0.0

2014-09-23

Ibland händer det mig att jag träffar på någon sak som får mig att tänka: "det här var ju pinsamt att jag inte kunde". Det händer inte så ofta. Inte på grund av att jag kan så mycket, utan mest för att jag är medveten om hur lite jag kan. Det blir därför inte så pinsamt att erkänna för mig själv och för andra att jag inte kan en viss sak. Men ibland händer det, som sagt var, att jag stöter på något som jag tycker att jag borde kunnat. Idag var en sådan dag.

Kollegan och jag diskuterade ett problem med att skicka UDP-trafik från en dator till en annan. Problemet vi hade var att trots att vi visste att meddelandena skickades från avsändardatorn kom de inte fram till UDP-servern vi startat på mottagardatorn. Jag hade dagen innan kollat upp om det inte var något brandväggsproblem, men det verkade det inte vara. Nu hade i alla fall kollegan löst problemet.

När jag frågade honom hur han löst problemet förklarade han att fixat det genom att starta om UDP-servern och speca dess host till 0.0.0.0 istället för localhost.
- "OK. Hur kunde det avhjälpa problemet?", undrade jag.
- "Startar man en server på adress 0.0.0.0 så lyssnar den på alla nätverksinterface på datorn. Inte bara ett specifikt. Det kan vara bra om det finns flera nätverksinterface på datorn, och så var det i det här fallet. Använde man localhost när man startade började servern lyssna på fel interface".

Aah. Det verkade ju verkligen användbart. Varför visste jag inte om det här? Jag hade sett adressen 0.0.0.0 vid olika tillfällen, men aldrig funderat över vad den kunde betyda. Nu fick jag en anledning att läsa på lite.

Surfade in på in på Wikipedia som jag brukar när jag vill läsa på lite kort om någon sak eller något ämne. Artikeln där listade flera olika användningsområden för 0.0.0.0 varav den sista i listan var just: "A way to specify any IPv4-interface at all". Det stog även att addressen när den användes på detta sätt kallades för INADDR_ANY.

Eftersom wikipediasidan var ganska kortfattad så googlade jag vidare på INADDR_ANY och kom då till man-sidan för IP i Linux Programmers Manual (länk här). På den sidan står följande att läsa om INADDR_ANY: "When INADDR_ANY is specified in the bind call, the socket will be bound to all local interfaces." Dvs precis det som kollegan hade sagt! Inte för att jag för ett ögonblick tvivlade på honon, men det är ju alltid bra att läsa på lite själv ;)

Summa summarum: idag har jag lärt mig att man kan binda till IP 0.0.0.0 för att få sin lyssnare att lyssna på alla lokala interface. Lite jobbigt att jag inte kände till det sedan länge, men det är väl bara att bryta ihop och komma igen som Per Elofsson sa ;)

Nyheter i Java 8 - del 6 - nyheter i klassen Arrays

2014-09-24

Nu är det dags för den sjätte delen i miniserien om nyheterna i Java 8. Förra delen handlade om parallell hantering av strömmar. Den här delen skall handla om de nya metoderna i klassen Arrays.

Nyheter i Arrays

I Java 8 kom det en hel del nyheter i klassen Arrays. Några av dessa underlättar parallell exekvering av operationer på arrayer. Vi skall kolla närmare på dessa nu. När vi ändå håller på, så passar vi på att ta en kik på de nya metoder som inte har med parallellism att göra också.

I korthet kan man säga att de nya metoderna i Arrays är följande:

  • <T extends Comparable<? super T>> void parallelSort(T[] a)
  • <T> void parallelPrefix(T[] array, BinaryOperator<T> op)
  • <T> void setAll(T[] array, IntFunction<? extends T> generator)
  • <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator)
  • <T> Spliterator spliterator(T[] array)
  • <T> Stream stream(T[] array)
Många av metoderna finns även i överlagrade versioner som hanterar arrayer av de primitiva typerna. Det finns i många fall också överlagrade versioner som tar ett startindex och ett slutindex av arrayen och då bara opererar på den angivna delen av arrayen. I genomgången nedan kommer det att noteras vilka överlagrade varianter av respektive metod som finns.

parallelSort()

Vad gör metoden

parallelSort() är, inte helt oväntat, en parallell version av Arrays.sort(). Dvs den sorterar den inkommande arrayen "in place".

Notera att parallelSort() använder JVM:ens fork-join-pool, med de nackdelerar detta innebär. Se diskussionen angående fork-join-poolen i förra delen av serien, då avseende dess användande i samband med parallella strömmar.

Exempel

Som ett litet exempel på den variant av parallelSort() som tar en int[] som inparameter kan vi använda följande lilla klass:

import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;

public class MainClass {
  private final static int ARRAY_SIZE = 10;
  public static void main(String[] args) {
    // Generate array of intetegers.
    Random random = new Random();
    IntStream intStream = random.ints(ARRAY_SIZE);
    int[] randomNumbers = intStream.toArray();

    // Sort it an measure how long it takes.
    System.out.println("Before: " + Arrays.toString(randomNumbers)); 
    long startTime = System.currentTimeMillis();        
    Arrays.parallelSort(randomNumbers);
    long endTime = System.currentTimeMillis();    
    System.out.println("Execution time: " + (endTime - startTime) + "ms"); 
    System.out.println("After: " + Arrays.toString(randomNumbers));
  }
}

Exemplet ovan använder den nya metoden ints(int size) i Random för att generera en IntStream innehållande size slumptal. Sedan görs talen i IntStream:en om till en array med hjälp av metoden IntStream.toArray(). Ganska smidigt sätt att generera en array med slumptal eller hur?

För att göra det tydligt i vilken klass som ints(int) och toArray() fanns användes lokala variabler spara returvärdena från dessa metoder. Undviker man det blir genereringen av arrayen med slumptal mycket kort och koncis:

int[] randomNumbers = new Random().ints(ARRAY_SIZE).toArray();

Resultatet av en körning av ovanstående program kan se ut så här:

Before: [2128764741, 1109516321, -1918587090, -1349245711, 194244467, 2121767928, -434729300, -830024849, -14951130, 369065649]
Execution time: 1ms
After: [-1918587090, -1349245711, -830024849, -434729300, -14951130, 194244467, 369065649, 1109516321, 2121767928, 2128764741]

Nu är det ju ingen större bedrift att sortera tio stycken heltal. Vad vi egentligen vill testa är om Arrays.parallelSort() exekverar snabbare än Arrays.sort() då indatat är mycket större än så. För att prova det så sätter vi istället ARRAY_SIZE till 100 miljoner och kör igen. (Fast vi passar på att kommentera bort utskriften av arrayen före och efter...) Resultatet blir:

Execution time: 33707ms

Byter vi ut Arrays.parallelSort() mot Arrays.sort() och kör igen blir resultatet:

Execution time: 32719ms
Det var inte riktigt det resultat vi ville ha ;) Nu exekverades dock programmet på min ytterst slöa bussdator. Kör vi istället testerna på min jobbdator blir tiderna 10671ms respektive 4184ms. Nu ser det mer ut som förväntat.

Överlagrade varianter

parallelSort() finns i följande överlagrade varianter:

  • <T extends Comparable<? super T>> void parallelSort(T[] a)
  • <T extends Comparable<? super T>> void parallelSort(T[] a, int fromIndex, int toIndex)
  • <T> void parallelSort(T[] a, Comparator<? super T> cmp)
  • <T> void parallelSort(T[] a, int fromIndex, int toIndex, Comparator<? super T> cmp)
Dessutom finns det överlagrade versioner av parallelSort() för byte[], short[], int[], long[], float[], double[] och char[] i två varianter: dels den varianten som bara tar in själva arrayen och dels i den varianten som även tar in ett startindex och ett slutindex.

parallelPrefix()

Vad gör metoden

parallelPrefix() är en metod som går igenom indataarrayen element för element och applicerar en BinaryOperator på varje element. Inparametrar till anropen till BinaryOperator är elementet i fråga samt närmast föregående element.

I engelskan verkar en sådan operation beskrivas som att man cumulates varje element genom att applicera en funktion. Jag har inte hittat något bra svenskt ord för det. Är det någon som har koll på vad det heter på svenska så får ni gärna tipsa mig. Det är alltid roligt att utöka sitt ordförråd :)

parallelPrefix() arbetar, inte helt överraskande givet dess namn, parallellt så den BinaryOperator som man anger till metoden bör vara sidoeffektsfri.

Exempel

Som ett exempel på parallelPrefix() skall vi skriva ett litet program som adderar varje element i en int-array med närmast föregående element. Programmet ser ut så här:

import java.util.Arrays;

public class Prefix {
  public static void main(String[] args) {
    int[] array = new int[]{1,2,3,4,5,6,7,8,9};
    Arrays.parallelPrefix(array, (a, b) -> a + b);
    System.out.println(Arrays.toString(array));
  }
}
...
[1, 3, 6, 10, 15, 21, 28, 36, 45]

Vi har här använt lambdauttrycket (a, b) -> a + b som BinaryOperator.

Överlagrade varianter

parallelSort() finns i följande överlagrade varianter:

  • <T> void parallelPrefix(T[] array, BinaryOperator<T> op)
  • <T> void parallelPrefix(T[] array, int fromIndex, int toIndex, BinaryOperator<T> op)
Utöver dessa finns det versioner av för double[], int[] och long[] i både den varianten som enbart tar en array som inparameter och den varianten som utöver arrayen även tar in start- och slutindex.

setAll()

Vad gör metoden

setAll() initialiserar alla elementen i en array till värden som ges av en generatorfunktion. Generatorfunktionens typ är olika för de olika överlagrade varianterna av setAll(). Den är IntUnaryFunction när det gäller den varianten som operarar på en int[], IntToDouble när det gäller variantern för double[] och så vidare.

För varje element i arrayen som skall initialiseras anropas generatorfunktionen med elementets index som parameter (det är därför generatorfunktionerna har typen IntToSomething). Detta index kan sedan användas, om så önskas, till att bestämma vilket värde som elementet skall få.

Exempel

Ett exemelprogram som iniatialiserar en liten int[] så att varje element får ett värde som är dubbelt så stort som elementets index kan se ut så här:


import java.util.Arrays;

public class SetAll {
  public static void main(String[] args) {
    int[] array = new int[10];
    Arrays.setAll(array, a -> a * 2);
    System.out.println(Arrays.toString(array));
  }
}
...
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Överlagrade varianter

setAll() finns i de här varianterna:

  • <T> void setAll(T[] array, IntFunction<? extends T> generator)
  • void setAll(double[] array, IntToDoubleFunction generator)
  • void setAll(int[] array, IntUnaryOperator generator)
  • void setAll(long[] array, IntToLongFunction generator)

parallelSetAll()

Vad gör metoden

parallelSetAll() gör samma sak som setAll(), dvs den initialiserar en array givet en generatorfunktion, men den gör det parallellt.

Exempel

Motsvarande exempel som ovan använde setAll() ser med parallelSetAll() ut så här:


import java.util.Arrays;

public class ParallelSetAll {
  public static void main(String[] args) {
    int[] array = new int[10];
    Arrays.parallelSetAll(array, a -> a * 2);
    System.out.println(Arrays.toString(array));
  }
}
...
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Vi ser att resultatet blev detsamma som i det föregående exemplet, vilket ju känns bra.

Överlagrade varianter

parallelSetAll() har motsvarande varianter som setAll() det vill säga:

  • <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator)
  • void parallelSetAll(double[] array, IntToDoubleFunction generator)
  • void parallelSetAll(int[] array, IntUnaryOperator generator)
  • void parallelSetAll(long[] array, IntToLongFunction generator)

spliterator()

Vad gör metoden

spliterator() returnerar en Spliterator för arrayen i fråga.

Vad är då en Spliterator? Ja, Spliterator är ett interface som enligt dokumentationen är tillför att, fritt översatt, "traversera och partionera elementen från en källa". Och vad betyder det då? Det betyder att Spliterator liknar Iterator på så sätt att Spliterator innehåller metoder för att stega igenom en uppsättning element på något sätt. Det finns dock två stora skillnader mellan Spliterator och Iterator.

Den första är att istället för att som Iterator ha separata metoder för att kolla om det finns fler element och för att hämta ut nästa element har Spliterator kombinerat dessa till samma metod som då tar en Consumer som appliceras på elementet om det finns något element kvar att applicera den på. Spliterator har två sådana metoder: void tryAdvance(Consumer<? super T> action) och void forEachRemaining(Consumer<? super T> action). Det som skiljer dessa två metoder åt är att tryAdvance() applicerar Consumer:n på nästa element medan forEachRemaining() applicerar den på alla resterande element.

Den andra och konceptuellt viktigaste skillnaden mellan Iterator och Spliterator är att det går att dela upp elementen i grupper och låta separata Spliterator:s hantera varje grupp. Det är detta som dokumentationen menar om när den talar om att partionera elementen. Uppdelningen i grupper görs med metoden Spliterator<T> trySplit() som returnerar en ny Spliterator som innehåller ett antal element från Spliterator:n på vilken trySplit() anropades. Notera att elementen efter anropet till trySplit() inte längre finns med i den ursprungliga Spliterator:n. Det är alltså frågan om en flytt av element, inte en kopierring. Notera också att man vid anropet till trySplilt() inte kan påverka vilka eller om ens några element som den Spliterator som returneras innehåller. Det är upp till implementationen av Spliterator:n att bestämma.

Vad är då poängen med att splitta? Den är att man på så sätt får möjlighet att parallellisera itereringen av elementen. Varje grupp kan itereras parallellt.

Den finns ett antal specialiseringar av Spliterator för de primitiva typerna double, int och long. Dessa är implementerade som inre-interface i Spliterator och de heter Spliterator.OfDouble, Spliterator.OfInt respektive Spliterator.OfLong.

Exempel

Det första exemplet visar hur forEachRemaining() kan användas för att behandla alla element i en array. I exemplet innebär hanteringen bara att elementet skrivs ut, men i verkliga livet kan givetvis valfria operationer göras i lambdauttrycket som ges som argument till forEachRemaining().

int[] oneToNine = new int[] {1,2,3,4,5,6,7,8,9};
Spliterator.OfInt spliterator = Arrays.spliterator(oneToNine);
spliterator.forEachRemaining((int i) -> System.out.println(i));
...
1
2
3
4
5
6
7
8
9

Det andra exemplet skriver först ut några av de inledande elementer mha tryAdvance() för att sedan anropa forEachRemaining() för att skriva ut resten.

System.out.println("Now we will run tryAdvance() a few times:"); 
int[] tenToNineteen = new int[] {10,11,12,13,14,15,16,17,18,19};
Spliterator.OfInt spliterator2 = Arrays.spliterator(tenToNineteen);
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
System.out.println("-----------------------------"); 
System.out.println("Now we will run forEachRemaining():"); 
spliterator2.forEachRemaining((int i) -> System.out.println(i));

...
Now we will run tryAdvance() a few times:
10
11
12
13
14
-----------------------------
Now we will run forEachRemaining():
15
16
17
18
19

I det sista exemplet så splittar vi en Spliterator i två delar och behandlar sen varje Spliterator var för sig.

int[] twentyToTwentynine = new int[] {20,21,22,23,24,25,26,27,28,29};
Spliterator.OfInt firstSpliterator = Arrays.spliterator(twentyToTwentynine);
Spliterator.OfInt secondSpliterator = firstSpliterator.trySplit();
System.out.println("First spliterator contains these numbers: "); 
firstSpliterator.forEachRemaining((int i) -> System.out.println(i));
System.out.println("Second spliterator contains these numbers: "); 
secondSpliterator.forEachRemaining((int i) -> System.out.println(i));
...
First spliterator contains these numbers: 
25
26
27
28
29
Second spliterator contains these numbers: 
20
21
22
23
24
Vi ser att trySplit() har delat elementen så att de lägsta talen splittas av till den andra Spliterator:n medan de högsta talen blir kvar i den ursprungliga. Detta är dock inget vi direkt kan påverka.

Överlagrade varianter

Följande varianter finns av Spliterator:

  • <T> Spliterator<T> spliterator(T[] array)
  • <T> Spliterator<T> spliterator(T[] array, int startInclusive, int endExclusive)
  • Spliterator.OfDouble spliterator(double[] array)
  • Spliterator.OfDouble spliterator(double[] array, int startInclusive, int endExclusive)
  • Spliterator.OfInt spliterator(int[] array)
  • Spliterator.OfInt spliterator(int[] array, int startInclusive, int endExclusive)
  • Spliterator.OfLong spliterator(long[] array)
  • Spliterator.OfLong spliterator(long[] array, int startInclusive, int endExclusive)

stream()

Vad gör metoden

Metoden stream() returnerar en ström som backas av elementen i arrayen som ges som parameter till metoden. Den ger oss alltså möjlighet att på ett lätt sätt gå från en (gammal, tråkig) array till en (ny och skinande) Stream och därigenom kunna utnyttja alla de nya och roliga saker man kan göra med en sådan ;)

Exempel

Vi har ju använt Stream:s in rad exempel tidigare, så följande exempel kommer inte att vara så spännande. Det visar mest på hur man kan komma över till en Strean från en array. Vad vi sedan kan göra med strömmen i fråga har vi ju avhandlat i flera av de tidigare delearna i serien. I exemplet skriver vi bara ut elementen i strömmen till standard out.

int[] oneTonNine = new int[] {2,4,6,8};
IntStream stream = Arrays.stream(oneTonNine);
stream.forEach(System.out::println);
...
2
4
6
8

Överlagrade varianter

De här överlagrade versionerna finns av stream():
  • <T> Stream<T> stream(T[] array)
  • <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive)
  • DoubleStream stream(double[] array)
  • DoubleStream stream(double[] array, int startInclusive, int endExclusive)
  • IntStream stream(int[] array)
  • IntStream stream(int[] array, int startInclusive, int endExclusive)
  • LongStream stream(long[] array)
  • LongStream stream(long[] array, int startInclusive, int endExclusive)

Avslutning del 6

I den här delen har vi gått igenom de metoder i Arrays som är nya i Java 8. De flesta metoder har något med parallellism att göra. Om jag får våga mig på en gissning så kommer stream() att vara den av de nya metoderna som man får mest nytta av. Med hjälp av den kan man behandla en arrays element som en ström och då få tillgång till allt godis som man kan göra med strömmar i Java 8.

En annan metod som jag kan tänka mig att man kan få nytta av är parallelSort(). Sortera gör man ju titt som tätt, så den kommer man säkert kunna använda.

Skall jag vara helt ärlig så är jag mer tveksam till hur ofta man kommer att använda de andra nya metoderna i Arrays. De känns inte lika användbara som de metoder jag nyss nämnde. Fast det är klart, kan spliterator() kombineras ihop på något bra sätt med ett ramverk som erbjuder parallell exekvering så kanske det blir användbart.

I nästa del, vilket antagligen blir den sista delen i serien, skall vi kolla lite översiktligt på övriga, icke-parallellism-relaterade nyheter i Java 8.

Tidigare delar i serien

Del 1: Interface
Del 2: Streams och lambda expressions
Del 3: Metoder i Stream, metod- och konstruktorreferenser
Del 4: Metoder klassen Collectors
Del 5: Parallella strömmar

Referenser

[1] API-dokumentationen för Arrays