Geschenkte Nebenläufigkeit dank JDK8?

JavaFX – Stream the GUI

Sven Ruppert
©Shutterstock.com/ilolab

Mit dem JDK8 ist die Streams-API verfügbar. Was bringt sie für den Entwickler? Wie wird sie verwendet? Wir beginnen heute mit einem Beispiel aus der JavaFX-Welt.

Der erste Beitrag zum Thema JavaFX im Developer Café wird sich mit der Verwendung der Streams-API innerhalb der GUI beschäftigen. Als Startpunkt verwende ich eines der offiziellen JavaFX-Beispiele von Oracle [1]. Meine Wahl ist auf das Line-Chart gefallen. Gehen wir im Folgenden davon aus, dass wir n Messwertereihen anzeigen müssen. Diese Messwerte sind alle 100-Zeiteinheiten aufgenommen worden (in unserem Fall per Random generiert) und sollen als Kurven dargestellt werden. Die Messwerte werden mittels Splines interpoliert. Hierzu verwende ich die commons-math3 Lib von Apache [2].

Die Messwertereihen sollen gleichzeitig im Line-Chart angezeigt werden. Dazu sind pro Kurve folgende Schritte notwendig. 

  • Hole (generiere) die Reihe der Messwerte
  • Berechne die interpolierten Werte
  • Erzeuge die graphischen Elemente
  • Befülle das Line-Chart

Hole (generiere) die Reihe der Messwerte

Für dieses Beispiel generieren wir uns die Messwerte. Die Datenstruktur soll aus einer Liste bestehen, in der pro Messwertreihe eine Liste von Integer-Werten enthalten ist: List<List<Integer>>. Traditionell geht man dabei wie folgt vor (Listing 1a).

public List <List<Integer>> generateDemoValueMatrix(){
final List <List<Integer>> result = new ArrayList<> ();
   for (int i = 0; i<200; i++){
   	final List<Integer> generatedDemoValuesForY = 
generateDemoValuesForY();
        	result.add(generatedDemoValuesForY);            
}
   	return result;
}
public List<Integer> generateDemoValuesForY(){
final Random random = new Random();
   final List<Integer> result = new ArrayList<> ();
   for(int i = 0; i<10; i++){
   	result.add(random.nextInt(100));
   }
return result;
}

Dank der neuen Streams-API können wir diese Konstrukte jedoch kompakter formulieren (Listing 1b). Hier ist noch keine Nebenläufigkeit im Spiel, jedoch kann man hier den Einsatz der neuen Streams-API sehen. Auffällig ist hier die Verwendung des Builder-Pattern, die eine sequentielle Beschreibung ermöglicht, die der Verwendung von Operatoren ähnlich ist.

public List<List<Integer>> generateDemoValueMatrix(){
return Stream
   	.generate(this::generateDemoValuesForY)
         .limit(200)	//Anzahl Kurven
         .collect(Collectors.toList());
}

public List<Integer> generateDemoValuesForY(){
final Random random = new Random();
   return Stream
   	.generate(() -> random.nextInt(100))
         .limit(10)
         .collect(Collectors.toList());
}

Berechne die interpolierten Werte

Die Werte werden hier mittels der comons-math3 Lib von Apache [2] berechnet. Das Vorgehen ist sehr geradlinig. Zu Beginn wird der Interpolations-Algorithmus ausgewählt und nachfolgend mit den vorhandenen Stützstellen initialisiert. Für jede Messreihe wird eine eigene Instanz von UnivariateFunction erzeugt, damit es bei der späteren parallelen Ausführung nicht zu ungewünschten Nebeneffekten kommt (Listing 2a).

private UnivariateFunction createInterpolateFunction(
final List<Integer> values){

   final double[] valueArrayX = new double[values.size()];
   for (int i = 0; i < valueArrayX.length; i++) {
   	valueArrayX[i] = (double)i* STEP_SIZE;
  	}

   final double[] valueArrayY = new double[values.size()];
   int i=0;
   for (final Integer value : values) {
valueArrayY[i] = (double) value.intValue();
         i= i+1;
  	}
   final UnivariateInterpolator interpolator = 
new SplineInterpolator();
   final UnivariateFunction function = 
interpolator.interpolate(valueArrayX, valueArrayY);
  	return function;
}

Die Berechnung der fehlenden Messwerte selbst (Listing 2b), wird jetzt parallel  (.parallelStream()) für jede Messwertereihe List<Double> durchgeführt (interpolateFunction.value(i)). Hier sieht man sofort, dass der syntaktische Aufwand zum Erreichen der Parallelität gegen Null geht. Mit einem einzigen Aufruf auf dem Stream werden die nachfolgenden Elemente in einem Root-Fork-Join-Pool ausgeführt. Die sequenzielle Version, die über die Liste der Messwertereihen iteriert um dann die Werte einer Reihe zu berechnen, benötigt demnach mehr Zeit.

private List<List<Double>> getValuesForSeries() {
final List<List<Integer>> demoValueMatrix =
generateDemoValueMatrix();
   final List<List<Double>> collect = demoValueMatrix
   	.parallelStream()
         	.map(v -> {
         		final UnivariateFunction interpolateFunction = 
createInterpolateFunction(v);
              	//baue Kurve auf
                final int anzahlValuesInterpolated = 
(v.size()-1) * STEP_SIZE;
               	final List<Double> result = new ArrayList<> ();
                for (int i = 0; i < anzahlValuesInterpolated-1; i++) {
                	final double valueForY = 
interpolateFunction.value(i);
                     	result.add(valueForY);
              	}
                return result;
       	})
         .collect(Collectors.toList());
return collect;
}

Erzeuge die graphischen Elemente

Da die Messwerte nun berechnet worden sind, können die Grafikelemente erzeugt werden, die dann der Instanz des Line-Charts übergeben werden. Auch hier wieder die parallele Verarbeitung pro Messwertereihe (Listing 3).

private List<XYChart.Series> generateNextSeries(){
final List<XYChart.Series> chartSeries = getValuesForSeries()
   	.parallelStream()
         	.map(v -> {
        		final XYChart.Series nextSeries = new XYChart.Series();
               	int i = 0;
                for (final Double valueForY : v) {
                	final XYChart.Data data 
= new XYChart.Data(i, valueForY);
                  	nextSeries.getData().add(data);
                     	i = i + 1;
              	}
                return nextSeries;
        	}).collect(Collectors.toList());
   return chartSeries;
}

Befülle das Line-Chart

Nun fehlt nur noch die Übergabe der Grafikelemente. Das ist ein sequenzieller Prozess und besteht lediglich aus der Iteration über die einfache Liste der einzelnen Messwertereihen. List<XYChart.Series> (Listing 4).

final List<XYChart.Series> serieses = generateNextSeries();
final ObservableList<XYChart.Series> data = lineChart.getData();
data.addAll(serieses);

Fazit

Mit der Verwendung der Streams-API kann man schon bei sehr einfachen Aufgaben eine höhere Nebenläufigkeit erreichen. Das Schöne daran ist die einfache Verwendung. Die Syntax lässt sehr klar erkennen welche Teile parallelisiert werden und welche nicht. Der Entwickler selbst muss sich nicht, wie bisher üblich, mit der Verwendung von Threads auseinandersetzen.

In diesem einfachen Beispiel dauerte die serielle Version (LineChartSerialDemo) bei dem 11´ten Durchlauf 2.799.209.417ns und die parallele Version (LineChartDemo)  benötigte bei dem 11´ten Durchlauf 261.545.220ns. Das ist immerhin ein SpeedUP von Faktor 10.

Was noch alles mit der Streams-API gemacht werden kann, werde ich in den nachfolgenden Beiträgen ausführlicher beschreiben. 

Die Quelltexte zu diesem Text sind unter [3] zu finden. Wer umfangreichere Beispiele zu diesem Thema sehen möchte, dem empfehle einen Blick auf [4] (Modul cdi-commons).

Aufmacherbild: Streams of light abstract Cool waves background von Shutterstock / Urheberrecht: ilolab

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.